from __future__ import annotations

from calendar import monthrange
from collections import Counter
from contextlib import contextmanager
from dataclasses import dataclass
from datetime import date, datetime, time, timedelta, timezone
from difflib import SequenceMatcher
import json
import logging
from pathlib import Path
import re
import sqlite3
from time import monotonic
from typing import Literal
import unicodedata
from zoneinfo import ZoneInfo

from fastapi import HTTPException
import httpx
from pydantic import BaseModel, Field, ValidationError, model_validator

from app.api.routes.google_workspace import (
    CatalogProduct,
    _build_catalog_doc_preview,
    _build_catalog_sheet_preview,
    _create_google_doc,
    _create_google_sheet,
    _extract_catalog_bucket,
    _generate_preview,
    _product_bucket_labels,
)
from app.core.config import get_settings
from app.models.google_workspace import GoogleWorkspaceCreateRequest, GoogleWorkspaceDocumentPreview, GoogleWorkspacePreviewRequest
from app.services.google_workspace_session import get_active_google_workspace_connection
from app.services.llm_client import describe_llm_http_error, request_llm_chat_completion
from app.services.tenant_store import SessionIdentity, TenantStaffUserCreatePayload, TimeclockOverviewQuery, get_tenant_store
from shared.assistant_profiles import list_allowed_tools_for_profile

CalendarDate = date
logger = logging.getLogger(__name__)

_CHAT_LIST_LIMIT = 5000
_AGENT_MAX_TOOL_CALLS = 5
_SYNTHESIS_MAX_LIST_ITEMS = 160
_SYNTHESIS_MAX_STRING_CHARS = 6000

_DETERMINISTIC_SYNTHESIS_TOOLS = {
    "create_google_workspace_document",
    "create_reservation",
    "update_reservation",
    "delete_reservation",
    "upsert_product",
    "write_suspended_order",
    "get_sales_goals",
    "write_sales_goal",
    "write_shared_note",
    "manage_tenant_user",
    "update_module_settings",
    "update_venue_profile",
}


_STOPWORDS = {
    "a",
    "ad",
    "ai",
    "al",
    "alla",
    "allo",
    "che",
    "ceh",
    "chi",
    "ci",
    "coi",
    "con",
    "da",
    "dal",
    "dalla",
    "dei",
    "degli",
    "del",
    "della",
    "delle",
    "dello",
    "di",
    "e",
    "ce",
    "gli",
    "ha",
    "hanno",
    "ho",
    "i",
    "il",
    "in",
    "la",
    "le",
    "li",
    "lo",
    "l",
    "marca",
    "marche",
    "mi",
    "mia",
    "mie",
    "miei",
    "mio",
    "ne",
    "nel",
    "nella",
    "nelle",
    "nello",
    "negli",
    "o",
    "s",
    "mostri",
    "mostra",
    "mostrami",
    "prezzo",
    "prezzi",
    "ultimo",
    "ultimi",
    "effettuato",
    "effettuati",
    "per",
    "acquistiamo",
    "acquisti",
    "comprare",
    "compriamo",
    "quale",
    "quali",
    "quanti",
    "quanto",
    "quanta",
    "quante",
    "costa",
    "costano",
    "costare",
    "volte",
    "sono",
    "su",
    "stasera",
    "tra",
    "un",
    "una",
    "nostra",
    "nostre",
    "nostri",
    "nostro",
    "anno",
    "scorso",
    "scorsa",
    "passato",
    "passata",
    "posso",
    "puoi",
    "puo",
    "potete",
    "possiamo",
    "si",
    "abbiamo",
    "avete",
    "hai",
    "tengo",
    "tiene",
    "teniamo",
    "tenete",
}

_GENERIC_PRODUCT_TOKENS = {
    "ACQUA",
    "AMARO",
    "APERITIVO",
    "BIRRA",
    "BITTER",
    "BRANDY",
    "CAFFE",
    "GIN",
    "LIQUORE",
    "RUM",
    "SCIROPPO",
    "SUCCHI",
    "SUCCO",
    "TEQUILA",
    "VERMOUTH",
    "VINO",
    "VODKA",
    "WHISKY",
}
_SUPPLIER_CATALOG_JAMAICAN_RUM_RULES = (
    ("appleton", "brand noto jamaicano: Appleton"),
    ("j wray", "brand noto jamaicano: J. Wray"),
    ("wray", "brand noto jamaicano: Wray & Nephew"),
    ("hampden", "brand noto jamaicano: Hampden"),
    ("worthy park", "brand noto jamaicano: Worthy Park"),
    ("smith cross", "brand noto jamaicano: Smith & Cross"),
    ("myers", "brand noto jamaicano: Myers's"),
    ("myer", "brand noto jamaicano: Myer's/Myers's"),
    ("coruba", "brand noto jamaicano: Coruba"),
    ("xaymaca", "indicazione Jamaica/Xaymaca nel nome"),
    ("jamaica", "origine Jamaica dichiarata nel nome"),
    ("jamaican", "origine Jamaican dichiarata nel nome"),
)
_KNOWN_SUPPLIER_CATALOG_QUERY_TOKENS = {
    "cavallaro",
    "diageo",
    "laconi",
    "martini",
    "moet",
    "montenegro",
    "reduzzi",
    "velier",
}

_DIRECT_GREETING_PATTERN = re.compile(r"^\s*(ciao|buongiorno|buonasera|salve|hey|ehi)\s*[.!?]*\s*$", re.IGNORECASE)
_CONFIRMATION_PATTERN = re.compile(r"^\s*(confermo|conferma|si confermo|ok|va bene|procedi|salva pure)\s*[.!?]*\s*$", re.IGNORECASE)
_JSON_BLOCK_PATTERN = re.compile(r"```(?:json)?\s*(\{.*\})\s*```", re.DOTALL | re.IGNORECASE)
_SUSPENDED_ORDER_PATTERN = re.compile(r"\b(ordine sospeso|sospeso)\b", re.IGNORECASE)
_RESERVATION_KEYWORDS = ("prenot", "copert", "tavol", "posto", "posti", "capienz", "sala", "piantin", "stasera", "domani", "oggi", "sera", "pranzo")
_RESERVATION_SUBJECT_KEYWORDS = ("prenot", "copert", "tavol", "posto", "posti", "capienz", "sala", "piantin")
_ORDERS_KEYWORDS = (
    "marca",
    "marche",
    "fornitor",
    "prodot",
    "compr",
    "acquist",
    "ordin",
    "spes",
    "import",
    "euro",
    "costo",
    "valore",
    "succh",
    "gin",
    "rum",
    "vodka",
    "birra",
    "vino",
)
_TIMECLOCK_KEYWORDS = (
    "cartellin",
    "timbr",
    "turn",
    "lavorat",
    "ore lavor",
    "entrat",
    "uscit",
    "inizio turno",
    "fine turno",
    "presenz",
)
_TIPS_KEYWORDS = (
    "mancia",
    "mance",
    "tips",
)
_INVENTORY_KEYWORDS = (
    "magazzin",
    "giacenz",
    "rimanenz",
    "scort",
    "stock",
    "inventario",
    "inventari",
)
_HOMEMADE_KEYWORDS = (
    "homemade",
    "prebatch",
    "preparazione",
    "preparazioni",
)
_ITALIAN_SMALL_NUMBERS = {
    "zero": 0,
    "un": 1,
    "uno": 1,
    "una": 1,
    "due": 2,
    "tre": 3,
    "quattro": 4,
    "cinque": 5,
    "sei": 6,
    "sette": 7,
    "otto": 8,
    "nove": 9,
    "dieci": 10,
}
_ITALIAN_ORDINAL_RANKS = {
    "primo": 1,
    "prima": 1,
    "primi": 1,
    "prime": 1,
    "secondo": 2,
    "seconda": 2,
    "secondi": 2,
    "seconde": 2,
    "terzo": 3,
    "terza": 3,
    "terzi": 3,
    "terze": 3,
    "quarto": 4,
    "quarta": 4,
    "quarti": 4,
    "quarte": 4,
    "quinto": 5,
    "quinta": 5,
    "quinti": 5,
    "quinte": 5,
    "sesto": 6,
    "sesta": 6,
    "sesti": 6,
    "seste": 6,
    "settimo": 7,
    "settima": 7,
    "settimi": 7,
    "settime": 7,
    "ottavo": 8,
    "ottava": 8,
    "ottavi": 8,
    "ottave": 8,
    "nono": 9,
    "nona": 9,
    "noni": 9,
    "none": 9,
    "decimo": 10,
    "decima": 10,
    "decimi": 10,
    "decime": 10,
}
_ITALIAN_MONTHS = {
    "gennaio": 1,
    "febbraio": 2,
    "marzo": 3,
    "aprile": 4,
    "maggio": 5,
    "giugno": 6,
    "luglio": 7,
    "agosto": 8,
    "settembre": 9,
    "ottobre": 10,
    "novembre": 11,
    "dicembre": 12,
}
_MONTH_REFERENCE_PATTERN = re.compile(
    r"\b("
    + "|".join(re.escape(label) for label in _ITALIAN_MONTHS)
    + r")(?:\s+(20\d{2}|scorso|scorsa|passato|passata|corrente|attuale|prossimo|prossima))?\b"
)
_PRODUCT_FAMILY_QUERY_TERMS: dict[str, set[str]] = {
    "vodka": {"vodka", "vodke"},
    "gin": {"gin"},
    "rum": {"rum", "rhum"},
    "whisky": {"whisky", "whiskey", "whiskies", "whiskeys"},
    "tequila": {"tequila", "tequile"},
    "mezcal": {"mezcal", "mezcals"},
    "amaro": {"amaro", "amari"},
    "vino": {"vino", "vini"},
    "birra": {"birra", "birre"},
    "succo": {"succo", "succhi"},
}
_PRODUCT_FAMILY_PRODUCT_HINTS: dict[str, set[str]] = {
    "vodka": {
        "vodka",
        "grey goose",
        "belvedere",
        "42 below",
        "ketel",
        "ketel one",
        "absolut",
        "skyy",
        "stoli",
        "stolichnaya",
        "ciroc",
        "beluga",
        "tito",
        "wyborowa",
    },
    "gin": {
        "gin",
        "bombay",
        "tanqueray",
        "oxley",
        "seatrus",
        "hendrick",
        "monkey 47",
        "mare",
    },
    "rum": {
        "rum",
        "gosling",
        "zacapa",
        "bacardi",
        "santa teresa",
        "plantation",
        "diplomatico",
        "havana",
        "don papa",
    },
    "whisky": {
        "whisky",
        "whiskey",
        "johnnie walker",
        "black label",
        "blue label",
        "jack daniel",
        "maker's mark",
        "lagavulin",
        "talisker",
        "woodford",
        "angel envy",
    },
    "tequila": {
        "tequila",
        "patron",
        "herradura",
        "don julio",
        "casamigos",
        "volcan",
    },
    "mezcal": {
        "mezcal",
        "vida",
        "casamigos mezcal",
    },
    "amaro": {
        "amaro",
        "montenegro",
        "averna",
        "branca",
        "lucano",
        "yuntaku",
        "ramazzotti",
    },
    "vino": {
        "vino",
        "prosecco",
        "champagne",
        "trebbiano",
        "pinot",
        "cabernet",
        "merlot",
        "chardonnay",
    },
    "birra": {
        "birra",
        "peroni",
        "ipa",
        "lager",
        "st stefanus",
    },
    "succo": {
        "succo",
        "succhi",
        "yoga",
        "derby",
        "derblue",
        "derbyblue",
        "naty",
        "naty's",
    },
}
_PRODUCT_FAMILY_IGNORED_TOKENS = {
    "articolo",
    "articoli",
    "catalogo",
    "cl",
    "litri",
    "litro",
    "locale",
    "lt",
    "ml",
    "miei",
    "mia",
    "mie",
    "mio",
    "nostri",
    "nostre",
    "nostro",
    "nostra",
    "prodotto",
    "prodotti",
    "frutta",
    "frutto",
    "volume",
    "volumi",
}
_PRODUCT_QUERY_IGNORED_TOKENS = {
    "a",
    "ad",
    "abbiamo",
    "acquistiamo",
    "aggiungi",
    "aggiungere",
    "alcun",
    "alcuna",
    "alcune",
    "alcuni",
    "agosto",
    "ai",
    "al",
    "alla",
    "alle",
    "allo",
    "anno",
    "aprile",
    "avete",
    "che",
    "cl",
    "confronta",
    "confrontare",
    "confronto",
    "cose",
    "cosa",
    "cerca",
    "cercare",
    "cercami",
    "cataloghi",
    "catalogo",
    "crea",
    "dammi",
    "dato",
    "dati",
    "dicembre",
    "dimmi",
    "dentro",
    "effettuata",
    "effettuate",
    "effettuati",
    "effettuato",
    "fai",
    "fammi",
    "fatta",
    "fatte",
    "fatti",
    "fatto",
    "febbraio",
    "fornitore",
    "fornitori",
    "gennaio",
    "giugno",
    "hai",
    "ha",
    "l",
    "litri",
    "litro",
    "lt",
    "luglio",
    "marca",
    "marche",
    "maggio",
    "marzo",
    "ml",
    "mostra",
    "mostrami",
    "mostri",
    "nei",
    "nelle",
    "negli",
    "nel",
    "nella",
    "nello",
    "novembre",
    "ordinare",
    "ordinata",
    "ordinato",
    "ordinati",
    "ordine",
    "ordini",
    "ottobre",
    "passata",
    "passato",
    "periodi",
    "periodo",
    "ce",
    "pezzi",
    "pezzo",
    "piu",
    "prodotti",
    "prodotto",
    "primi",
    "primo",
    "qualche",
    "qualsiasi",
    "quante",
    "quanti",
    "quanto",
    "quantita",
    "riga",
    "righe",
    "rispetto",
    "ritorna",
    "ritornami",
    "scorsa",
    "scorso",
    "s",
    "settembre",
    "solo",
    "sui",
    "sugli",
    "sul",
    "sulla",
    "sulle",
    "sullo",
    "unit",
    "units",
    "pack",
    "packs",
    "parlando",
    "percentuale",
    "percentuali",
    "contiene",
    "contengono",
    "sospeso",
    "storico",
    "tra",
    "totale",
    "totali",
    "trova",
    "trovare",
    "trovami",
    "trovi",
    "tutta",
    "tutte",
    "tutti",
    "tutto",
    "vedere",
    "vedi",
    "meno",
    "volte",
    "volume",
    "volumi",
    "vs",
    "versus",
}
_PURCHASE_QUERY_IGNORED_TOKENS = _PRODUCT_QUERY_IGNORED_TOKENS | {
    "acquisto",
    "acquisti",
    "acquistare",
    "acquistata",
    "acquistate",
    "acquistati",
    "acquistato",
    "articolo",
    "articoli",
    "all",
    "allora",
    "compra",
    "comprare",
    "confronta",
    "confrontami",
    "confrontare",
    "confronto",
    "confronti",
    "contro",
    "comprensiva",
    "comprensive",
    "comprensivi",
    "comprensivo",
    "comprata",
    "comprate",
    "comprati",
    "comprato",
    "costosa",
    "costose",
    "costosi",
    "costoso",
    "economica",
    "economiche",
    "economici",
    "economico",
    "elenca",
    "elencami",
    "elenco",
    "merce",
    "mese",
    "mesi",
    "fatta",
    "fatte",
    "fatti",
    "fatto",
    "ct",
    "bt",
    "ik",
    "pz",
    "stato",
    "stata",
    "stati",
    "state",
    "volta",
    "lista",
    "non",
    "ordina",
    "ordinano",
    "ordinate",
    "ordinato",
    "ordiniamo",
    "ordino",
    "penultima",
    "penultime",
    "penultimi",
    "penultimo",
    "prezzo",
    "prezzi",
    "proditti",
    "prima",
    "prime",
    "primi",
    "primo",
    "secondo",
    "secondi",
    "seconda",
    "seconde",
    "terza",
    "terze",
    "terzi",
    "terzo",
    "quarta",
    "quarte",
    "quarti",
    "quarto",
    "quinta",
    "quinte",
    "quinti",
    "quinto",
    "sesta",
    "seste",
    "sesti",
    "sesto",
    "settima",
    "settime",
    "settimi",
    "settimo",
    "ottava",
    "ottave",
    "ottavi",
    "ottavo",
    "nona",
    "none",
    "noni",
    "nono",
    "decima",
    "decime",
    "decimi",
    "decimo",
    "separata",
    "separate",
    "separati",
    "separato",
    "settimana",
    "settimane",
    "ultima",
    "ultime",
    "ultimi",
    "ultimo",
    "voglio",
    "vorrei",
    "speso",
    "spesa",
    "spendere",
    "soldi",
    "euro",
    "eur",
    "importo",
    "importi",
    "valore",
    "valori",
    "costo",
    "costi",
    "adesso",
    "attuale",
    "attuali",
    "attualmente",
    "finora",
    "ora",
    "oggi",
    "quando",
    "quest",
    "questa",
    "queste",
    "questi",
    "questo",
    "riispetto",
    "rispetto",
}
_INVENTORY_QUERY_IGNORED_TOKENS = _PRODUCT_QUERY_IGNORED_TOKENS | {
    "casa",
    "elenca",
    "elencami",
    "elenco",
    "bottiglia",
    "bottiglie",
    "cartone",
    "cartoni",
    "inferiore",
    "inferiori",
    "lista",
    "magazzino",
    "magazzini",
    "non",
    "giacenza",
    "giacenze",
    "rimanenza",
    "rimanenze",
    "scorta",
    "scorte",
    "stock",
    "inventario",
    "inventari",
    "disponibilita",
    "disponibile",
    "disponibili",
    "consumo",
    "consumi",
    "consumata",
    "consumate",
    "consumato",
    "consumati",
    "usata",
    "usate",
    "usato",
    "usati",
    "giorno",
    "giorni",
    "singola",
    "singole",
    "singoli",
    "singolo",
    "sotto",
    "unita",
}
_TIPS_QUERY_IGNORED_TOKENS = _PRODUCT_QUERY_IGNORED_TOKENS | {
    "mancia",
    "mance",
    "tip",
    "tips",
    "somma",
    "sommare",
    "sommano",
    "fai",
    "fammi",
    "calcola",
    "calcolami",
    "preso",
    "presa",
    "prese",
    "prendere",
    "sorico",
    "giornata",
    "giornate",
    "giorno",
    "giorni",
    "storico",
    "totale",
    "totali",
    "caricato",
    "caricata",
    "caricati",
    "caricate",
    "consegnato",
    "consegnata",
    "consegnati",
    "consegnate",
    "incassato",
    "incassata",
    "incassati",
    "incassate",
    "sala",
    "bar",
}
_TIPS_PERSON_QUERY_IGNORED_TOKENS = _TIPS_QUERY_IGNORED_TOKENS | {
    "analisi",
    "cartella",
    "chiamalo",
    "chiamala",
    "chiamato",
    "chiamata",
    "con",
    "crea",
    "creami",
    "csv",
    "dettagli",
    "dettagliata",
    "dettagliate",
    "dettagliati",
    "dettagliato",
    "dettaglio",
    "dipendente",
    "dipendenti",
    "doc",
    "documento",
    "documenti",
    "drive",
    "file",
    "foglio",
    "folder",
    "google",
    "gli",
    "i",
    "il",
    "la",
    "le",
    "lista",
    "elenco",
    "lo",
    "miei",
    "mie",
    "mia",
    "mio",
    "mostra",
    "mostrami",
    "nome",
    "ogni",
    "prepara",
    "preparare",
    "preparami",
    "prepari",
    "prospetto",
    "rendiconto",
    "report",
    "resoconto",
    "riepilogo",
    "salva",
    "salvare",
    "salvami",
    "sheet",
    "situazione",
    "staff",
    "tabella",
    "titolo",
    "tutta",
    "tutte",
    "tutti",
    "tutto",
    "un",
    "una",
    "uno",
    "voglio",
    "vorrei",
    "xlsx",
}
_TIPS_REPORT_FRAGMENTS = (
    "analisi mance",
    "elenco mance",
    "lista mance",
    "prospetto mance",
    "rendiconto mance",
    "report mance",
    "resoconto mance",
    "riepilogo mance",
    "situazione mance",
    "storico mance",
    "storico delle mance",
    "dettaglio mance",
    "dettaglio delle mance",
    "mance dettaglio",
)
_TIPS_STAFF_BREAKDOWN_FRAGMENTS = (
    "per ogni dipendente",
    "per ciascun dipendente",
    "per dipendente",
    "per tutti i dipendenti",
    "per tutto lo staff",
    "per ogni membro dello staff",
    "per ogni persona",
    "per ciascuno",
    "per ognuno",
)
_EMAIL_PATTERN = re.compile(r"\b[A-Z0-9._%+\-]+@[A-Z0-9.\-]+\.[A-Z]{2,}\b", re.IGNORECASE)
_PHONE_PATTERN = re.compile(r"(\+?\d[\d\s().\-\/]{5,}\d)")
_RESERVATION_CREATE_ASSISTANT_HINTS = (
    "per creare la prenotazione mi servono ancora",
    "dimmi quale devo usare per la prenotazione",
)
_PRODUCT_WRITE_ASSISTANT_HINTS = (
    "per registrare il prodotto",
    "se li hai, aggiungi anche",
    "se qualche dato ti manca, lo salvo comunque",
    "restano da completare",
    "aggiorno subito la scheda",
    "quante unita contiene un cartone",
)
_SALES_GOAL_ASSISTANT_HINTS = (
    "per aggiungere un nuovo obiettivo di vendita avrei bisogno",
    "per impostare un nuovo obiettivo di vendita avrei bisogno",
    "per salvare un obiettivo mi serve almeno",
    "per questo obiettivo mi serve anche il target numerico",
    "per un obiettivo numerico indicami almeno il prodotto o il fornitore da monitorare",
    "per un doppio target mi servono target primario",
    "per essere preciso vuoi impostare l obiettivo",
)
_PENDING_PRODUCT_LOT_CODE = "DA COMPLETARE"
_PENDING_PRODUCT_SUPPLIER_NAME = "DA COMPLETARE"
_PRODUCT_WRITE_NAME_NOISE = {
    "devo",
    "voglio",
    "vorrei",
    "aggiungi",
    "aggiungere",
    "crea",
    "creare",
    "inserisci",
    "inserire",
    "registra",
    "registrare",
    "salva",
    "salvare",
    "metti",
    "mettere",
    "serve",
    "servono",
    "mi",
    "un",
    "una",
    "uno",
    "nuovo",
    "nuova",
    "altro",
    "altra",
    "prodotto",
    "prodotti",
    "articolo",
    "articoli",
    "catalogo",
}


class PlannedToolCall(BaseModel):
    tool: Literal[
        "get_locale_profile",
        "search_products",
        "upsert_product",
        "get_purchase_overview",
        "compare_purchase_periods",
        "get_purchase_frequency",
        "get_purchase_batches",
        "get_purchase_history",
        "get_suspended_order",
        "write_suspended_order",
        "get_sales_goals",
        "write_sales_goal",
        "list_shared_notes",
        "write_shared_note",
        "get_reservations_snapshot",
        "list_reservations",
        "create_reservation",
        "update_reservation",
        "delete_reservation",
        "create_google_workspace_document",
        "get_module_settings",
        "update_module_settings",
        "list_fiscal_documents",
        "list_tenant_users",
        "get_timeclock_summary",
        "get_inventory_consumption",
        "get_homemade_recipe",
        "manage_tenant_user",
        "update_venue_profile",
        "describe_tenant_schema",
        "run_tenant_query",
    ]
    arguments: dict[str, object] = Field(default_factory=dict)


AssistantSurface = Literal["home", "documents"]


class AssistantPlan(BaseModel):
    mode: Literal["reply", "tool"]
    reply: str | None = None
    tool_calls: list[PlannedToolCall] = Field(default_factory=list)
    confidence: float | None = Field(default=None, ge=0.0, le=1.0)
    needs_clarification: bool = False

    @model_validator(mode="after")
    def validate_plan(self) -> "AssistantPlan":
        if self.mode == "reply":
            if not self.reply or not self.reply.strip():
                raise ValueError("reply obbligatoria in modalita reply")
            return self

        if not self.tool_calls:
            raise ValueError("tool_calls obbligatorie in modalita tool")
        return self


@dataclass
class OperationalAssistantRun:
    reply: str
    model: str
    route: str
    trace: dict[str, object]
    thread_state: dict[str, object] | None = None


@dataclass
class GoogleWorkspacePreviewRun:
    preview: GoogleWorkspaceDocumentPreview
    route: str
    model: str
    trace: dict[str, object]


class SearchProductsArgs(BaseModel):
    query: str = ""
    limit: int = Field(default=_CHAT_LIST_LIMIT, ge=1, le=_CHAT_LIST_LIMIT)


class ProductWriteArgs(BaseModel):
    operation: Literal["upsert", "delete"] = "upsert"
    product_name: str | None = None
    lot_code: str | None = None
    supplier_name: str | None = None
    product_code: str | None = None
    final_price_vat: float | None = Field(default=None, ge=0)
    vat_rate: float | None = Field(default=None, ge=0)
    weight_kg: float | None = Field(default=None, ge=0)
    unit_price_per_kg: float | None = Field(default=None, ge=0)
    category: str | None = None
    notes: str | None = None
    units_per_pack: float | None = Field(default=None, ge=0)
    liters_per_unit: float | None = Field(default=None, ge=0)


class PurchaseOverviewArgs(BaseModel):
    query: str = ""
    year: int | None = Field(default=None, ge=2020, le=2100)
    month: int | None = Field(default=None, ge=1, le=12)
    start_date: date | None = None
    end_date: date | None = None
    limit: int = Field(default=_CHAT_LIST_LIMIT, ge=1, le=_CHAT_LIST_LIMIT)


class PurchaseComparisonArgs(BaseModel):
    query: str = ""
    primary_year: int = Field(ge=2020, le=2100)
    primary_month: int | None = Field(default=None, ge=1, le=12)
    secondary_year: int = Field(ge=2020, le=2100)
    secondary_month: int | None = Field(default=None, ge=1, le=12)
    focus_hint: Literal["products", "orders", "quantity", "amount"] | None = None
    percentage_requested: bool = False
    limit: int = Field(default=_CHAT_LIST_LIMIT, ge=1, le=_CHAT_LIST_LIMIT)


class PurchaseFrequencyArgs(BaseModel):
    query: str = ""
    year: int | None = Field(default=None, ge=2020, le=2100)
    month: int | None = Field(default=None, ge=1, le=12)
    start_date: date | None = None
    end_date: date | None = None
    limit: int = Field(default=_CHAT_LIST_LIMIT, ge=1, le=_CHAT_LIST_LIMIT)


class PurchaseHistoryArgs(BaseModel):
    query: str = ""
    year: int | None = Field(default=None, ge=2020, le=2100)
    month: int | None = Field(default=None, ge=1, le=12)
    start_date: date | None = None
    end_date: date | None = None
    limit: int = Field(default=_CHAT_LIST_LIMIT, ge=1, le=_CHAT_LIST_LIMIT)


class PurchaseBatchesArgs(BaseModel):
    query: str = ""
    batch_id: int | None = Field(default=None, ge=1)
    target_date: date | None = None
    year: int | None = Field(default=None, ge=2020, le=2100)
    month: int | None = Field(default=None, ge=1, le=12)
    start_date: date | None = None
    end_date: date | None = None
    sort_order: Literal["latest", "earliest"] = "latest"
    limit: int = Field(default=_CHAT_LIST_LIMIT, ge=1, le=_CHAT_LIST_LIMIT)


class SuspendedOrderReadArgs(BaseModel):
    staff: str | None = None


class SuspendedOrderItemArgs(BaseModel):
    product_query: str = Field(min_length=1)
    quantity: int = Field(ge=1, le=500)


class SuspendedOrderWriteArgs(BaseModel):
    operation: Literal["set", "add"] = "set"
    items: list[SuspendedOrderItemArgs]
    staff: str | None = None


class SalesGoalsArgs(BaseModel):
    year: int | None = Field(default=None, ge=2020, le=2100)


class SharedNotesArgs(BaseModel):
    limit: int = Field(default=_CHAT_LIST_LIMIT, ge=1, le=_CHAT_LIST_LIMIT)


class ReservationsSnapshotArgs(BaseModel):
    target_date: date | None = None
    target_time: time | None = None
    time_window: Literal["all_day", "lunch", "evening"] = "all_day"
    customer_query: str = ""
    limit: int = Field(default=_CHAT_LIST_LIMIT, ge=1, le=_CHAT_LIST_LIMIT)


class ReservationCreateArgs(BaseModel):
    customer_name: str | None = None
    customer_phone: str | None = None
    customer_email: str | None = None
    reservation_date: date | None = None
    start_time: time | None = None
    guests: int | None = Field(default=None, ge=1, le=120)
    duration_minutes: int = Field(default=120, ge=1, le=600)
    notes: str | None = None
    area_preference: str | None = None
    status: Literal["pending", "confirmed", "seated", "completed", "cancelled", "no_show"] = "confirmed"
    source: Literal["manual", "whatsapp", "web"] = "manual"


class ReservationUpdateArgs(BaseModel):
    reservation_id: int | None = None
    customer_query: str = ""
    customer_phone: str | None = None
    target_date: date | None = None
    target_time: time | None = None
    new_customer_name: str | None = None
    new_customer_phone: str | None = None
    new_customer_email: str | None = None
    new_reservation_date: date | None = None
    new_start_time: time | None = None
    new_duration_minutes: int | None = Field(default=None, ge=1, le=600)
    new_guests: int | None = Field(default=None, ge=1, le=120)
    new_notes: str | None = None
    new_area_preference: str | None = None
    new_status: Literal["pending", "confirmed", "seated", "completed", "cancelled", "no_show"] | None = None


class ReservationDeleteArgs(BaseModel):
    reservation_id: int | None = None
    customer_query: str = ""
    customer_phone: str | None = None
    target_date: date | None = None
    target_time: time | None = None


class GoogleWorkspaceDocumentArgs(BaseModel):
    kind: Literal["doc", "sheet"] = "doc"
    title: str | None = Field(default=None, max_length=200)
    prompt: str | None = Field(default=None, max_length=4000)
    destination_folder_id: str | None = Field(default=None, max_length=255)


class SharedNoteWriteArgs(BaseModel):
    operation: Literal["create", "update", "delete"] = "create"
    note_id: int | None = None
    match_text: str = ""
    text: str | None = None
    author: str | None = None


class SalesGoalWriteArgs(BaseModel):
    operation: Literal["upsert", "delete"] = "upsert"
    goal_id: int | None = None
    year: int | None = Field(default=None, ge=2020, le=2100)
    name: str | None = None
    goal_type: Literal["quantity", "liters", "liters_dual", "note"] | None = None
    description: str | None = None
    product_match: str | None = None
    secondary_product_match: str | None = None
    supplier_match: str | None = None
    target: float | None = Field(default=None, ge=0)
    secondary_target: float | None = Field(default=None, ge=0)
    unit_label: str | None = None
    bonus_label: str | None = None


class GetModuleSettingsArgs(BaseModel):
    module: Literal["ordini", "prenotazioni", "whatsapp", "fiscal", "llm"] = "ordini"


class UpdateModuleSettingsArgs(BaseModel):
    module: Literal["prenotazioni", "whatsapp", "fiscal", "llm"] = "prenotazioni"
    settings: dict[str, object] = Field(default_factory=dict)


class ListReservationsArgs(BaseModel):
    date: CalendarDate | None = None
    limit: int = Field(default=_CHAT_LIST_LIMIT, ge=1, le=_CHAT_LIST_LIMIT)


class ManageTenantUserArgs(BaseModel):
    operation: Literal["create", "delete"] = "create"
    user_id: str | None = None
    name: str | None = None
    username: str | None = None
    email: str | None = None
    phone_number: str | None = None
    password: str | None = None
    permissions: list[str] = Field(default_factory=list)


class TimeclockSummaryArgs(BaseModel):
    query_text: str = ""
    scope: Literal["today", "week", "all", "active"] = "today"
    target_date: date | None = None
    start_date: date | None = None
    end_date: date | None = None
    limit: int | None = Field(default=_CHAT_LIST_LIMIT, ge=1, le=_CHAT_LIST_LIMIT)
    include_entries: bool = False


class InventoryConsumptionArgs(BaseModel):
    query: str = ""
    start_date: date | None = None
    end_date: date | None = None
    limit: int = Field(default=_CHAT_LIST_LIMIT, ge=1, le=_CHAT_LIST_LIMIT)


class HomemadeRecipeArgs(BaseModel):
    query: str = ""
    target_liters: float | None = Field(default=None, gt=0)


class UpdateVenueProfileArgs(BaseModel):
    venue_name: str | None = Field(default=None, max_length=200)
    address: str | None = Field(default=None, max_length=400)
    phone_number: str | None = Field(default=None, max_length=50)
    whatsapp_number: str | None = Field(default=None, max_length=50)


class DescribeTenantSchemaArgs(BaseModel):
    include_examples: bool = True


class RunTenantQueryArgs(BaseModel):
    sql: str = Field(min_length=1, max_length=6000)
    limit: int = Field(default=_CHAT_LIST_LIMIT, ge=1, le=_CHAT_LIST_LIMIT)


@dataclass
class ProductCandidate:
    id: int
    product_name: str
    lot_code: str
    supplier_name: str
    product_code: str | None
    final_price_vat: float | None
    weight_kg: float | None
    unit_price_per_kg: float | None
    category: str | None
    units_per_pack: float | None
    liters_per_unit: float | None
    score: float
    last_ordered_at: str | None = None
    total_quantity: int = 0


def _assistant_timezone() -> ZoneInfo:
    return ZoneInfo(get_settings().assistant_timezone)


def _now_in_timezone() -> datetime:
    return datetime.now(_assistant_timezone())


def _today_in_timezone() -> date:
    return _now_in_timezone().date()


def _normalize_text(value: str) -> str:
    normalized = unicodedata.normalize("NFKD", value or "").encode("ascii", "ignore").decode("ascii")
    lowered = normalized.lower()

    def convert_centiliters(match: re.Match[str]) -> str:
        raw_number = match.group(1).replace(",", ".")
        try:
            liters = float(raw_number) / 100.0
        except ValueError:
            return match.group(0)
        rendered = f"{liters:.2f}".rstrip("0").rstrip(".")
        return f"{rendered}l"

    lowered = re.sub(r"(\d+(?:[.,]\d+)?)\s*cl\b", convert_centiliters, lowered)
    lowered = lowered.replace("’", "'")
    lowered = re.sub(r"[^a-z0-9]+", " ", lowered)
    lowered = re.sub(r"\bdom\s*perignon\b", " dom peri ", lowered)
    lowered = re.sub(r"\bdp\b", " dom peri ", lowered)
    return " ".join(lowered.split())


def _humanize_operational_error_detail(detail: str | None) -> str:
    cleaned = " ".join((detail or "").split()).strip(" .")
    if not cleaned:
        return "Richiesta non completabile in modo affidabile"

    lowered = cleaned.casefold()
    if "errore comunicazione llm" in lowered or "endpoint llm" in lowered or "servizio llm" in lowered:
        return "In questo momento il servizio AI non e disponibile correttamente"
    if "<html" in lowered or "<!doctype" in lowered or "ngrok" in lowered:
        return "In questo momento il servizio AI non e disponibile correttamente"
    if len(cleaned) > 180:
        return "In questo momento non riesco a completare la richiesta in modo affidabile"
    return cleaned


def _tokenize_query(value: str) -> list[str]:
    tokens = [token for token in _normalize_text(value).split(" ") if token and token not in _STOPWORDS]
    if tokens:
        return tokens
    return [token for token in _normalize_text(value).split(" ") if token]


def _token_stem(token: str) -> str:
    cleaned = token.strip()
    if len(cleaned) <= 4:
        return cleaned
    if cleaned.endswith(("chi", "ghi", "che", "ghe")) and len(cleaned) > 5:
        return cleaned[:-2]
    if cleaned.endswith("s") and len(cleaned) > 4:
        return cleaned[:-1]
    if cleaned.endswith(("a", "e", "i", "o")):
        return cleaned[:-1]
    return cleaned


def _query_token_variants(token: str) -> list[str]:
    variants: list[str] = []
    for candidate in (token, _token_stem(token)):
        candidate = candidate.strip()
        if candidate and candidate not in variants:
            variants.append(candidate)
    return variants


def _tokens_match(left: str, right: str) -> bool:
    if left == right:
        return True

    left_stem = _token_stem(left)
    right_stem = _token_stem(right)
    if len(left_stem) >= 4 and left_stem == right_stem:
        return True

    minimum_length = min(len(left), len(right))
    if minimum_length >= 5:
        prefix_length = 3 if minimum_length >= 6 else 2
        if left[:prefix_length] == right[:prefix_length] and SequenceMatcher(None, left, right).ratio() >= 0.88:
            return True

    return False


def _score_product_match(query: str, searchable: str) -> float:
    score = _score_text_match(query, searchable)
    requested_families = _extract_requested_product_families(query)
    if not requested_families:
        return score

    product_families = _detect_product_families(searchable)
    family_matches = requested_families & product_families
    if family_matches:
        score += 4.0 * len(family_matches)
    return score


def _purchase_query_required_tokens(query: str) -> list[str]:
    tokens = [
        token
        for token in _tokenize_query(query)
        if token
        and not re.fullmatch(r"20\d{2}", token)
        and token not in _PURCHASE_QUERY_IGNORED_TOKENS
        and any(character.isalpha() for character in token)
    ]
    if len(tokens) < 2:
        return tokens

    distinctive_tokens = [token for token in tokens if not _is_product_family_query_token(token)]
    return distinctive_tokens or tokens


def _purchase_query_decimal_formats(query: str) -> list[tuple[str, str]]:
    tokens = _normalize_text(query).split()
    formats: list[tuple[str, str]] = []
    for left, right in zip(tokens, tokens[1:]):
        if re.fullmatch(r"\d", left) and re.fullmatch(r"\d{1,2}", right):
            formats.append((left, right))
    return formats


def _searchable_matches_decimal_format(searchable: str, decimal_format: tuple[str, str]) -> bool:
    left, right = decimal_format
    normalized = _normalize_text(searchable)
    pattern = rf"\b{re.escape(left)}\s+0*{re.escape(right)}(?:\s*(?:l|lt|litro|litri))?\b"
    return bool(re.search(pattern, normalized))


def _searchable_matches_purchase_query(searchable: str, query: str) -> bool:
    required_tokens = _purchase_query_required_tokens(query)
    if required_tokens and not _searchable_matches_all_query_tokens(searchable, required_tokens):
        return False

    decimal_formats = _purchase_query_decimal_formats(query)
    if decimal_formats and not all(
        _searchable_matches_decimal_format(searchable, decimal_format)
        for decimal_format in decimal_formats
    ):
        return False

    return True


def _score_purchase_product_match(query: str, searchable: str) -> float:
    if query.strip() and not _searchable_matches_purchase_query(searchable, query):
        return 0.0
    return _score_product_match(query, searchable)


def _catalog_query_tokens(query: str) -> list[str]:
    return [
        token
        for token in _tokenize_query(query)
        if token
        and not re.fullmatch(r"20\d{2}", token)
        and token not in _PRODUCT_QUERY_IGNORED_TOKENS
    ]


def _is_product_family_query_token(token: str) -> bool:
    return any(
        any(_tokens_match(token, alias) for alias in aliases)
        for aliases in _PRODUCT_FAMILY_QUERY_TERMS.values()
    )


def _significant_catalog_query_tokens(query: str) -> list[str]:
    tokens = _catalog_query_tokens(query)
    if len(tokens) < 2:
        return tokens

    distinctive_tokens = [token for token in tokens if not _is_product_family_query_token(token)]
    return distinctive_tokens or tokens


def _searchable_matches_all_query_tokens(searchable: str, query_tokens: list[str]) -> bool:
    searchable_tokens = _tokenize_query(searchable)
    return bool(query_tokens) and all(
        any(_tokens_match(query_token, searchable_token) for searchable_token in searchable_tokens)
        for query_token in query_tokens
    )


def _coerce_positive_float(value: object | None) -> float | None:
    try:
        parsed = float(value)
    except (TypeError, ValueError):
        return None
    if parsed <= 0:
        return None
    return parsed


def _extract_liters_from_text(value: str) -> float | None:
    normalized = value.lower().replace(",", ".")
    for pattern, multiplier in (
        (r"(\d+(?:\.\d+)?)\s*(?:lt|litri|litro|l)\b", 1.0),
        (r"(\d+(?:\.\d+)?)\s*cl\b", 0.01),
        (r"(\d+(?:\.\d+)?)\s*ml\b", 0.001),
    ):
        match = re.search(pattern, normalized)
        if not match:
            continue
        try:
            amount = float(match.group(1))
        except ValueError:
            continue
        liters = amount * multiplier
        if liters > 0:
            return liters
    return None


def _resolve_liters_per_unit(*, product_name: str, lot_code: str, liters_per_unit: object | None) -> float | None:
    explicit = _coerce_positive_float(liters_per_unit)
    if explicit is not None:
        return explicit
    for source in (lot_code, product_name):
        estimated = _extract_liters_from_text(source or "")
        if estimated is not None:
            return estimated
    return None


def _estimate_total_liters(
    *,
    quantity: int,
    product_name: str,
    lot_code: str,
    units_per_pack: object | None,
    liters_per_unit: object | None,
) -> float | None:
    resolved_liters = _resolve_liters_per_unit(
        product_name=product_name,
        lot_code=lot_code,
        liters_per_unit=liters_per_unit,
    )
    if resolved_liters is None:
        return None
    multiplier = _coerce_positive_float(units_per_pack) or 1.0
    return resolved_liters * float(quantity) * multiplier


def _lot_requires_units_per_pack(lot_code: str | None) -> bool:
    normalized = _normalize_text(lot_code or "")
    return normalized in {"ct", "cartone", "cartoni", "cassa", "casse"}


def _purchase_item_equivalent_units(item: dict[str, object]) -> float | None:
    quantity = _coerce_positive_float(item.get("total_quantity"))
    if quantity is None:
        return None
    if not _lot_requires_units_per_pack(str(item.get("lot_code") or "")):
        return quantity
    units_per_pack = _coerce_positive_float(item.get("units_per_pack"))
    return quantity * units_per_pack if units_per_pack is not None else None


def _is_liters_request(normalized: str) -> bool:
    return any(fragment in normalized for fragment in ("quanti litri", "quanto litro")) or _contains_normalized_word(
        normalized,
        "litri",
        "litro",
        "volume",
        "volumi",
    )


def _is_purchase_amount_request(normalized: str) -> bool:
    if any(
        fragment in normalized
        for fragment in (
            "quanto ho speso",
            "quanto abbiamo speso",
            "spesa totale",
            "totale speso",
            "valore ordini",
            "valore acquisti",
            "importo ordini",
            "importo acquisti",
            "in euro",
        )
    ):
        return True
    return _contains_normalized_word(
        normalized,
        "speso",
        "spesa",
        "spendere",
        "euro",
        "eur",
        "importo",
        "importi",
        "valore",
        "valori",
        "costo",
        "costi",
    )


def _is_purchase_quantity_request(normalized: str) -> bool:
    if _is_liters_request(normalized) or _is_purchase_amount_request(normalized):
        return False
    return _contains_normalized_word(
        normalized,
        "quanto",
        "quanta",
        "quanti",
        "quante",
        "quantita",
        "pezzo",
        "pezzi",
        "unita",
        "bottiglia",
        "bottiglie",
        "cartone",
        "cartoni",
    )


def _is_fiscal_spend_request(message: str, normalized: str) -> bool:
    if not _is_purchase_amount_request(normalized):
        return False
    if _contains_normalized_word(normalized, "ordine", "ordini") and not _contains_normalized_word(
        normalized,
        "pagato",
        "pagata",
        "pagati",
        "pagate",
        "fattura",
        "fatture",
        "bolla",
        "bolle",
        "ddt",
        "documento",
        "documenti",
    ):
        return False
    return _contains_normalized_word(
        normalized,
        "speso",
        "spesa",
        "spendere",
        "pagato",
        "pagata",
        "pagati",
        "pagate",
        "costo",
        "costi",
        "importo",
        "importi",
        "fattura",
        "fatture",
    )


def _extract_fiscal_spend_query(message: str) -> str:
    return (
        _extract_purchase_query(message).strip()
        or _extract_catalog_query(message).strip()
        or _extract_product_query(message).strip()
    )


def _latest_fiscal_spend_request_message(conversation: list[dict[str, str]]) -> str:
    for item in reversed(conversation[-10:]):
        if item.get("role") != "user":
            continue
        content = str(item.get("content") or "")
        normalized = _normalize_text(content)
        if _is_fiscal_spend_request(content, normalized):
            return content
    return ""


def _fiscal_document_token_condition(alias: str, token: str) -> str:
    return (
        "("
        f"lower(COALESCE(d.supplier_name, '')) LIKE '%{token}%' "
        f"OR lower(COALESCE(d.display_name, '')) LIKE '%{token}%' "
        f"OR lower(COALESCE(d.summary_text, '')) LIKE '%{token}%'"
        ")"
    )


def _fiscal_line_token_condition(alias: str, token: str) -> str:
    base_condition = (
        "("
        f"lower(COALESCE(i.description, '')) LIKE '%{token}%' "
        f"OR lower(COALESCE(i.product_code, '')) LIKE '%{token}%' "
        f"OR lower(COALESCE(i.raw_row_text, '')) LIKE '%{token}%' "
        f"OR lower(COALESCE(d.supplier_name, '')) LIKE '%{token}%' "
        f"OR lower(COALESCE(d.display_name, '')) LIKE '%{token}%'"
        ")"
    )
    if alias == "krug":
        # MOET abbreviates Krug as "KG" in DDT rows (e.g. "KG 2013*", "KG GRANDE CUVEE").
        moet_document_condition = (
            "("
            "lower(COALESCE(d.supplier_name, '')) LIKE '%moet%' "
            "OR lower(COALESCE(d.display_name, '')) LIKE '%moet%'"
            ")"
        )
        moet_kg_line_condition = (
            "("
            "lower(COALESCE(i.description, '')) = 'kg' "
            "OR lower(COALESCE(i.description, '')) LIKE 'kg %' "
            "OR lower(COALESCE(i.raw_row_text, '')) LIKE '% kg %'"
            ")"
        )
        return (
            "("
            f"{base_condition} "
            f"OR ({moet_document_condition} AND {moet_kg_line_condition})"
            ")"
        )
    return base_condition


def _is_total_only_request(normalized: str) -> bool:
    if "totale" not in normalized:
        return False
    if any(
        fragment in normalized
        for fragment in (
            "solo il totale",
            "solo totale",
            "soltanto il totale",
            "solamente il totale",
            "dammi solo",
            "dimmi solo",
            "fammi solo",
            "senza dettaglio",
            "senza dettagli",
            "niente dettaglio",
            "niente dettagli",
            "non dettagliare",
            "non fare elenco",
            "non farmi elenco",
            "non fare lista",
            "non farmi lista",
        )
    ):
        return True
    return _contains_normalized_word(normalized, "solo", "soltanto", "solamente") and _contains_normalized_word(
        normalized,
        "totale",
        "totali",
    )


def _format_liters(value: float | int | None) -> str | None:
    if value is None:
        return None
    rendered = f"{float(value):.2f}".rstrip("0").rstrip(".").replace(".", ",")
    return f"{rendered} L"


def _format_duration_hours(value: float | int | None) -> str | None:
    if value is None:
        return None
    total_minutes = max(int(round(float(value) * 60)), 0)
    hours = total_minutes // 60
    minutes = total_minutes % 60
    return f"{hours}h {minutes:02d}m"


def _format_italian_date_label(value: object | None) -> str:
    if isinstance(value, date):
        return value.strftime("%d/%m/%Y")
    text = str(value or "").strip()
    if not text:
        return ""
    try:
        return date.fromisoformat(text[:10]).strftime("%d/%m/%Y")
    except ValueError:
        return text


def _estimate_total_amount(*, quantity: int, final_price_vat: object | None) -> float | None:
    resolved_price = _coerce_positive_float(final_price_vat)
    if resolved_price is None:
        return None
    return round(float(quantity) * resolved_price, 2)



def _score_text_match(query: str, candidate: str) -> float:
    normalized_query = _normalize_text(query)
    normalized_candidate = _normalize_text(candidate)
    if not normalized_query or not normalized_candidate:
        return 0.0

    collapsed_query = normalized_query.replace(" ", "")
    collapsed_candidate = normalized_candidate.replace(" ", "")
    collapsed_bonus = 0.0
    if collapsed_query and collapsed_candidate:
        if collapsed_query == collapsed_candidate:
            collapsed_bonus = 3.0
        elif collapsed_query in collapsed_candidate:
            collapsed_bonus = 1.5

    query_tokens = _tokenize_query(query)
    candidate_tokens = normalized_candidate.split()
    overlap = sum(1 for token in query_tokens if any(_tokens_match(token, candidate_token) for candidate_token in candidate_tokens))
    phrase_bonus = 2.0 if " " in normalized_query and normalized_query in normalized_candidate else 0.0
    token_prefix_bonus = 0.0
    for token in query_tokens:
        if len(token) < 3:
            continue
        if any(candidate_token.startswith(token) for candidate_token in candidate_tokens):
            token_prefix_bonus += 1.0
    if query_tokens and overlap == 0 and not phrase_bonus and not token_prefix_bonus and collapsed_bonus <= 0:
        return 0.0
    ratio = SequenceMatcher(None, normalized_query, normalized_candidate).ratio()
    return overlap * 2.0 + phrase_bonus + token_prefix_bonus + collapsed_bonus + ratio


def _goal_name_anchor(value: str | None) -> str:
    normalized = _normalize_text(value or "")
    if not normalized:
        return ""
    normalized = re.sub(r"^(?:target|obiettivo|goal)\s+", "", normalized).strip()
    normalized = re.sub(r"\b20\d{2}\b$", "", normalized).strip()
    return normalized


def _product_bucket_labels_for_candidate(product: ProductCandidate) -> set[str]:
    return _product_bucket_labels(
        CatalogProduct(
            id=product.id,
            product_name=product.product_name,
            lot_code=product.lot_code,
            supplier_name=product.supplier_name,
            product_code=product.product_code,
            final_price_vat=product.final_price_vat,
            category=product.category,
        )
    )


def _extract_requested_product_families(query: str) -> set[str]:
    query_tokens = _tokenize_query(query)
    if not query_tokens:
        return set()

    requested_families: set[str] = set()
    residual_tokens: list[str] = []
    for token in query_tokens:
        if token in _PRODUCT_FAMILY_IGNORED_TOKENS:
            continue
        matched_family = None
        for family, aliases in _PRODUCT_FAMILY_QUERY_TERMS.items():
            if any(_tokens_match(token, alias) for alias in aliases):
                matched_family = family
                requested_families.add(family)
                break
        if matched_family is None:
            residual_tokens.append(token)

    if residual_tokens:
        return set()
    return requested_families


def _detect_product_families(searchable: str) -> set[str]:
    normalized_searchable = _normalize_text(searchable)
    searchable_tokens = normalized_searchable.split()
    detected: set[str] = set()
    for family, hints in _PRODUCT_FAMILY_PRODUCT_HINTS.items():
        for hint in hints:
            normalized_hint = _normalize_text(hint)
            if not normalized_hint:
                continue
            if " " in normalized_hint:
                if normalized_hint in normalized_searchable:
                    detected.add(family)
                    break
            elif any(_tokens_match(normalized_hint, candidate_token) for candidate_token in searchable_tokens):
                detected.add(family)
                break
    return detected


def _extract_likely_brand(product_name: str) -> str | None:
    raw_tokens = [token for token in re.split(r"[^A-Za-z0-9']+", product_name.upper()) if token]
    if not raw_tokens:
        return None

    while raw_tokens and raw_tokens[0] in _GENERIC_PRODUCT_TOKENS:
        raw_tokens.pop(0)

    brand_tokens: list[str] = []
    for token in raw_tokens:
        if re.fullmatch(r"\d+(?:[.,]\d+)?[A-Z]*", token):
            break
        if token in {"BT", "CL", "CT", "G", "KG", "L", "LT", "ML"}:
            break
        brand_tokens.append(token)
        if len(brand_tokens) >= 2:
            break

    if not brand_tokens:
        return None
    return " ".join(brand_tokens).strip()


def _parse_json_object(raw_text: str) -> dict[str, object]:
    cleaned = raw_text.strip()
    if not cleaned:
        raise ValueError("Risposta JSON vuota")

    match = _JSON_BLOCK_PATTERN.search(cleaned)
    if match:
        cleaned = match.group(1).strip()

    try:
        parsed = json.loads(cleaned)
        if isinstance(parsed, dict):
            return parsed
    except json.JSONDecodeError:
        pass

    start = cleaned.find("{")
    end = cleaned.rfind("}")
    if start == -1 or end == -1 or end <= start:
        raise ValueError("Oggetto JSON non trovato")

    parsed = json.loads(cleaned[start : end + 1])
    if not isinstance(parsed, dict):
        raise ValueError("Il payload JSON non e' un oggetto")
    return parsed


def _normalize_plan_payload(payload: dict[str, object]) -> dict[str, object]:
    if "mode" in payload:
        return payload

    if "tool" in payload:
        return {
            "mode": "tool",
            "reply": None,
            "tool_calls": [
                {
                    "tool": payload.get("tool"),
                    "arguments": payload.get("arguments") or {},
                }
            ],
        }

    tool_calls = payload.get("tool_calls")
    if isinstance(tool_calls, list):
        return {
            "mode": "tool",
            "reply": None,
            "tool_calls": tool_calls,
        }

    reply = payload.get("reply")
    if isinstance(reply, str) and reply.strip():
        return {"mode": "reply", "reply": reply.strip(), "tool_calls": []}

    return payload


def _default_staff(session: SessionIdentity) -> str:
    raw_value = session.user_name or session.username or session.user_email or session.tenant_name
    return raw_value.strip().lower()


def _serialize_datetime(value: str | None) -> str | None:
    if value is None:
        return None
    return value.replace(" ", "T")


@contextmanager
def _connect_orders_database(session: SessionIdentity):
    database_path = Path(session.database_path)
    connection = sqlite3.connect(database_path)
    connection.row_factory = sqlite3.Row
    connection.execute("PRAGMA foreign_keys = ON;")
    try:
        yield connection
    finally:
        connection.close()


@contextmanager
def _connect_orders_database_readonly(session: SessionIdentity):
    database_path = Path(session.database_path).resolve()
    connection = sqlite3.connect(f"file:{database_path}?mode=ro", uri=True)
    connection.row_factory = sqlite3.Row
    try:
        yield connection
    finally:
        connection.close()


_TENANT_QUERY_TIMEOUT_SECONDS = 5.0
_TENANT_QUERY_ALLOWED_ROWS_DEFAULT = 200
_TENANT_QUERY_DISALLOWED_PATTERN = re.compile(
    r"\b(insert|update|delete|drop|alter|create|replace|truncate|attach|detach|pragma|vacuum|reindex|grant|revoke|begin|commit|rollback)\b",
    re.IGNORECASE,
)


def _quote_sqlite_ident(name: str) -> str:
    return '"' + str(name).replace('"', '""') + '"'


def _json_safe_cell(value: object) -> object:
    if value is None:
        return None
    if isinstance(value, bool):
        return int(value)
    if isinstance(value, (int, float, str)):
        return value
    if isinstance(value, (date, datetime, time)):
        return value.isoformat()
    return json.dumps(value, ensure_ascii=False)


def _infer_sqlite_column_type(values: list[object]) -> str:
    has_real = False
    has_int = False
    for value in values:
        if value is None:
            continue
        if isinstance(value, bool):
            has_int = True
            continue
        if isinstance(value, int):
            has_int = True
            continue
        if isinstance(value, float):
            has_real = True
            continue
        return "TEXT"
    if has_real:
        return "REAL"
    if has_int:
        return "INTEGER"
    return "TEXT"


def _create_query_table_from_rows(connection: sqlite3.Connection, table_name: str, rows: list[dict[str, object]]) -> None:
    if not rows:
        return

    column_names: list[str] = []
    seen: set[str] = set()
    for row in rows:
        for key in row.keys():
            if key not in seen:
                seen.add(key)
                column_names.append(key)
    if not column_names:
        return

    definitions: list[str] = []
    for key in column_names:
        values = [_json_safe_cell(row.get(key)) for row in rows]
        definitions.append(f"{_quote_sqlite_ident(key)} {_infer_sqlite_column_type(values)}")
    connection.execute(f"CREATE TABLE {_quote_sqlite_ident(table_name)} ({', '.join(definitions)})")

    placeholders = ", ".join("?" for _ in column_names)
    columns_sql = ", ".join(_quote_sqlite_ident(key) for key in column_names)
    payload = [tuple(_json_safe_cell(row.get(key)) for key in column_names) for row in rows]
    connection.executemany(
        f"INSERT INTO {_quote_sqlite_ident(table_name)} ({columns_sql}) VALUES ({placeholders})",
        payload,
    )


def _ensure_query_table(
    connection: sqlite3.Connection,
    table_name: str,
    columns: list[tuple[str, str]],
) -> None:
    if _table_exists(connection, table_name):
        return
    definitions = ", ".join(
        f"{_quote_sqlite_ident(column_name)} {column_type}"
        for column_name, column_type in columns
    )
    connection.execute(f"CREATE TABLE {_quote_sqlite_ident(table_name)} ({definitions})")


def _table_exists(connection: sqlite3.Connection, table_name: str) -> bool:
    row = connection.execute(
        "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1",
        (table_name,),
    ).fetchone()
    return row is not None


def _rows_from_sqlite_table(connection: sqlite3.Connection, table_name: str) -> list[dict[str, object]]:
    if not _table_exists(connection, table_name):
        return []
    return [dict(row) for row in connection.execute(f"SELECT * FROM {_quote_sqlite_ident(table_name)}").fetchall()]


def _tenant_query_allows_permission(session: SessionIdentity, permission: str) -> bool:
    return get_tenant_store().session_has_assistant_scope(session, permission)


def _safe_iso_date(value: object) -> date | None:
    if isinstance(value, date):
        return value
    raw = str(value or "").strip()
    if not raw:
        return None
    try:
        return date.fromisoformat(raw)
    except ValueError:
        return None


def _homemade_calendar_rules_from_settings_value(value: object) -> list[dict[str, object]]:
    raw = str(value or "").strip()
    if not raw:
        return []
    try:
        decoded = json.loads(raw)
    except json.JSONDecodeError:
        return []
    rules = decoded.get("rules") if isinstance(decoded, dict) else decoded if isinstance(decoded, list) else []
    if not isinstance(rules, list):
        return []
    normalized_rules: list[dict[str, object]] = []
    today = _today_in_timezone()
    fallback_start = today - timedelta(days=730)
    fallback_end = today + timedelta(days=730)
    for item in rules:
        if not isinstance(item, dict):
            continue
        start_date = _safe_iso_date(item.get("start_date")) or fallback_start
        end_date = _safe_iso_date(item.get("end_date")) or fallback_end
        if start_date > end_date:
            continue
        raw_weekdays = item.get("weekdays")
        weekdays = {
            int(weekday)
            for weekday in (raw_weekdays if isinstance(raw_weekdays, list) else [])
            if isinstance(weekday, (int, float)) and 0 <= int(weekday) <= 6
        }
        normalized_rules.append(
            {
                "start_date": start_date,
                "end_date": min(end_date, start_date + timedelta(days=1095)),
                "weekdays": weekdays or set(range(7)),
            }
        )
    return normalized_rules


def _homemade_operational_day_rows(settings_rows: list[dict[str, object]]) -> list[dict[str, object]]:
    settings = settings_rows[0] if settings_rows else {}
    seen: set[tuple[str, str]] = set()
    rows: list[dict[str, object]] = []
    for usage_scope, field_name in (
        ("bar", "bar_calendar_json"),
        ("restaurant", "restaurant_calendar_json"),
    ):
        rules = _homemade_calendar_rules_from_settings_value(settings.get(field_name))
        for rule in rules:
            current = rule["start_date"]
            end_date = rule["end_date"]
            weekdays = rule["weekdays"]
            if not isinstance(current, date) or not isinstance(end_date, date) or not isinstance(weekdays, set):
                continue
            while current <= end_date:
                if current.weekday() in weekdays:
                    key = (usage_scope, current.isoformat())
                    if key not in seen:
                        seen.add(key)
                        rows.append({"usage_scope": usage_scope, "operational_date": current.isoformat()})
                current += timedelta(days=1)
    return rows


def _tenant_query_allowed_tips_areas(session: SessionIdentity) -> list[str]:
    store = get_tenant_store()
    allowed_areas: list[str] = []
    if store.session_has_assistant_scope(session, "tips_sala"):
        allowed_areas.append("sala")
    if store.session_has_assistant_scope(session, "tips_bar"):
        allowed_areas.append("bar")
    if not allowed_areas and session.role in {"owner", "super_admin"}:
        return ["sala", "bar"]
    return allowed_areas


_ASSISTANT_SCOPE_LABELS: dict[str, str] = {
    "ordini": "Ordini",
    "prenotazioni": "Prenotazioni",
    "documents": "Documenti",
    "menu": "Menu",
    "homemade": "Homemade",
    "fiscal_documents": "Documenti fiscali",
    "timeclock": "Turni",
    "inventory": "Inventario",
    "tips_sala": "Mance sala",
    "tips_bar": "Mance bar",
}

_ORDERS_ASSISTANT_TOOLS = {
    "search_products",
    "upsert_product",
    "get_purchase_overview",
    "compare_purchase_periods",
    "get_purchase_frequency",
    "get_purchase_batches",
    "get_purchase_history",
    "get_suspended_order",
    "write_suspended_order",
    "get_sales_goals",
    "write_sales_goal",
    "list_shared_notes",
    "write_shared_note",
}

_PRENOTAZIONI_ASSISTANT_TOOLS = {
    "get_reservations_snapshot",
    "create_reservation",
    "update_reservation",
    "delete_reservation",
    "list_reservations",
}


def _assistant_scope_allows(session: SessionIdentity, scope: str) -> bool:
    return get_tenant_store().session_has_assistant_scope(session, scope)


def _format_assistant_scope_labels(scopes: list[str]) -> str:
    labels = [_ASSISTANT_SCOPE_LABELS.get(scope, scope) for scope in scopes]
    if not labels:
        return "questa funzione"
    if len(labels) == 1:
        return labels[0]
    if len(labels) == 2:
        return f"{labels[0]} e {labels[1]}"
    return f"{', '.join(labels[:-1])} e {labels[-1]}"


def _assistant_scope_access_denied_run(
    *,
    normalized_thread_state: dict[str, object],
    message: str,
    missing_scopes: list[str],
) -> OperationalAssistantRun:
    allowed_label = _format_assistant_scope_labels(missing_scopes)
    verb = "abilitarlo" if len(missing_scopes) == 1 else "abilitarli"
    return OperationalAssistantRun(
        reply=(
            f"Non possiedi l'autorizzazione per usare l'Assistente su {allowed_label}. "
            f"Chiedi all'amministratore del locale di {verb} dal pannello Account."
        ),
        model="policy",
        route="assistant-scope-access-denied",
        trace={
            "surface": "home",
            "message": message,
            "reason": "assistant_scope_missing",
            "missing_scopes": missing_scopes,
            "thread_state_before": _snapshot_trace_value(normalized_thread_state),
        },
        thread_state=normalized_thread_state,
    )


def _is_timeclock_request(normalized: str) -> bool:
    if any(keyword in normalized for keyword in _TIMECLOCK_KEYWORDS):
        return True
    if not _contains_normalized_word(normalized, "ore"):
        return False
    if any(
        fragment in normalized
        for fragment in (
            "quante ore",
            "quanto ore",
            "ore hanno",
            "ore ha",
            "ore fatto",
            "ore fatta",
            "ore fatti",
            "ore fatte",
            "ore totali",
            "totali ore",
            "totale delle ore",
            "totali delle ore",
            "ore di lavoro",
            "totale ore di lavoro",
            "totali ore di lavoro",
            "totale ore",
            "ore dei ragazzi",
            "ore dei dipendenti",
            "ore del personale",
            "ore i ragazzi",
        )
    ):
        return True
    return (
        _contains_normalized_word(normalized, "ragazzi", "staff", "dipendente", "dipendenti", "personale", "team")
        and any(fragment in normalized for fragment in ("oggi", "ieri", "domani", "settimana", "mese", "anno", "periodo", "totale", "totali", "dettaglio", "dettagli", "singolarmente"))
    )


def _is_tips_request(message: str, normalized: str) -> bool:
    if not any(keyword in normalized for keyword in _TIPS_KEYWORDS):
        return False
    return True


def _is_tips_total_request(normalized: str) -> bool:
    return any(
        fragment in normalized
        for fragment in (
            "in totale",
            "totale mance",
            "totale delle mance",
            "somma del totale",
            "fai la somma",
            "fammi una somma",
            "somma totale",
            "quanto abbiamo fatto in totale",
            "quanto abbiamo preso in totale",
        )
    ) or (
        "totale" in normalized
        and any(keyword in normalized for keyword in ("mance", "mancia", "tips", "somma"))
    )


def _is_inventory_consumption_request(message: str, normalized: str) -> bool:
    if _is_homemade_stock_consumption_request(normalized):
        return False
    if _is_inventory_consumption_estimation_request(normalized):
        return False
    if not any(keyword in normalized for keyword in ("consumat", "consumo", "usat", "finite", "finito", "finite")):
        return False
    if _contains_normalized_word(normalized, "ordine", "ordini", "acquisto", "acquisti", "comprato", "compriamo"):
        return False
    query = _extract_inventory_query(message)
    if query:
        return True
    return any(hint in normalized for hints in _PRODUCT_FAMILY_PRODUCT_HINTS.values() for hint in hints)


def _is_inventory_consumption_estimation_request(normalized: str) -> bool:
    if _is_homemade_stock_consumption_request(normalized):
        return False
    has_estimation_cue = any(
        fragment in normalized
        for fragment in (
            "stim",
            "giornal",
            "giorno",
            "al giorno",
            "quotidian",
            "medio",
            "media",
            "calcol",
            "preved",
        )
    )
    if "consum" in normalized and has_estimation_cue:
        return True
    if has_estimation_cue and any(
        fragment in normalized
        for fragment in (
            "non come giacenza",
            "non la giacenza",
            "non giacenza",
            "storico ordini",
            "stock iniziale",
        )
    ):
        return True
    return False


def _has_explicit_inventory_estimation_scope(message: str, normalized: str) -> bool:
    if _extract_reference_year(message) is not None or _extract_reference_month(message) is not None:
        return True
    if _extract_reference_periods(message) or _extract_reference_week_range(message) is not None:
        return True
    return any(
        fragment in normalized
        for fragment in (
            "inventario",
            "inventari",
            "giacenz",
            "rimanenz",
            "magazzin",
            "stock iniziale",
            "stock finale",
            "storico ordini",
            "ordine",
            "ordini",
            "acquist",
            "comprat",
        )
    )


def _is_homemade_context_switch(normalized: str) -> bool:
    return any(
        fragment in normalized
        for fragment in (
            "homemade",
            "prebatch",
            "prep",
            "preparaz",
            "fatte in casa",
            "fatta in casa",
        )
    )


def _is_homemade_stock_consumption_request(normalized: str) -> bool:
    has_homemade_subject = any(
        fragment in normalized
        for fragment in (
            "homemade",
            "prebatch",
            "prep",
            "preparaz",
            "fatte in casa",
            "fatta in casa",
        )
    )
    if not has_homemade_subject:
        return False
    if any(
        fragment in normalized
        for fragment in (
            "consum",
            "cnsum",
            "scaric",
            "andamento",
            "come vanno",
            "come va",
        )
    ):
        return True
    return _contains_normalized_word(normalized, "dato", "dati", "riepilogo", "situazione") and any(
        fragment in normalized
        for fragment in (
            "oggi",
            "ieri",
            "giorn",
            "settimana",
            "mese",
            "anno",
            "periodo",
        )
    )


def _is_capability_question(normalized: str) -> bool:
    return any(
        fragment in normalized
        for fragment in (
            "sei in grado",
            "saresti in grado",
            "riesci a",
            "e possibile",
            "si puo",
            "potresti",
        )
    )


def _is_inventory_stock_quantity_request(message: str, normalized: str) -> bool:
    if not _contains_normalized_word(normalized, "quanto", "quanta", "quanti", "quante"):
        return False
    if not _contains_normalized_word(normalized, "abbiamo", "ho", "hai", "resta", "restano", "rimane", "rimangono"):
        return False
    if (
        _is_fiscal_documents_request(normalized)
        or _is_tips_request(message, normalized)
        or _is_timeclock_request(normalized)
        or _is_prenotazioni_request(message, normalized)
        or _is_homemade_request(message, normalized)
    ):
        return False
    if _contains_normalized_word(normalized, "ordine", "ordini", "acquisto", "acquisti", "comprato", "comprati"):
        return False
    return bool(_extract_inventory_query(message))


def _is_inventory_request(message: str, normalized: str) -> bool:
    if _is_inventory_consumption_estimation_request(normalized):
        return False
    if _is_inventory_consumption_request(message, normalized):
        return True
    if _is_inventory_author_request(normalized):
        return True
    if any(keyword in normalized for keyword in _INVENTORY_KEYWORDS):
        return True
    if _is_historical_purchase_request(message, normalized):
        return False
    if _is_inventory_stock_quantity_request(message, normalized):
        return True
    return " in casa" in f" {normalized} " or "a magazzino" in normalized


def _is_homemade_request(message: str, normalized: str) -> bool:
    if any(keyword in normalized for keyword in _HOMEMADE_KEYWORDS):
        return True
    if _contains_normalized_word(normalized, "ricetta", "ricette", "preparazione", "preparazioni") and _extract_homemade_query(message):
        return True
    if _contains_normalized_word(normalized, "prep", "batch"):
        return True
    if any(fragment in normalized for fragment in ("come faccio", "come si fa", "parti", "proporzion")) and _extract_liters_from_text(message):
        return True
    if "ricette" in normalized and any(fragment in normalized for fragment in ("abbiamo", "elenco", "lista", "mostra", "mostrami", "quali")):
        return True
    return False


def _is_fiscal_documents_request(normalized: str) -> bool:
    return any(
        fragment in normalized
        for fragment in ("fattur", "fiscal", "documenti fisc", "bolla", "bolle", "ddt")
    )


def _is_prenotazioni_request(message: str, normalized: str) -> bool:
    if _is_reservation_write_request(normalized):
        return True
    if _is_reservation_subject_request(normalized):
        return True
    return any(keyword in normalized for keyword in _RESERVATION_KEYWORDS)


def _is_menu_request(normalized: str) -> bool:
    return any(
        fragment in normalized
        for fragment in (
            "cocktail",
            "cocktail list",
            "drink",
            "piatt",
            "abbinament",
            "menu",
            "carta vini",
            "wine pairing",
        )
    )


def _is_orders_request(message: str, normalized: str) -> bool:
    if (
        _is_inventory_request(message, normalized)
        or _is_homemade_request(message, normalized)
        or _is_prenotazioni_request(message, normalized)
        or _is_menu_request(normalized)
        or _is_fiscal_spend_request(message, normalized)
    ):
        return False
    if _is_fiscal_documents_request(normalized) or _is_tips_request(message, normalized) or _is_timeclock_request(normalized):
        return False
    if any(keyword in normalized for keyword in _ORDERS_KEYWORDS):
        return True
    if any(
        (
            _is_historical_purchase_request(message, normalized),
            _is_purchase_comparison_request(message, normalized),
            _is_purchase_history_request(normalized),
            _is_purchase_product_list_request(normalized),
            _is_latest_batches_request(normalized),
            _is_purchase_time_request(normalized),
            _is_purchase_batch_detail_request(message, normalized),
            _is_price_request(normalized),
            _is_lowest_price_request(normalized),
            _is_price_per_weight_request(normalized),
            _is_missing_catalog_price_request(normalized),
            _is_catalog_data_request(normalized),
            _is_units_per_pack_request(normalized),
            _is_sales_goal_write_request(normalized),
            _is_sales_goal_read_request(normalized),
            _is_sales_goal_graph_request(normalized),
            _is_note_write_request(normalized),
            _is_suspended_order_write_request(message, normalized),
            _is_product_write_request(message, normalized),
            _contains_goal_keyword(normalized),
        )
    ):
        return True
    query = _extract_catalog_query(message)
    return _is_catalog_request(message, normalized, query)


def _assistant_required_scopes_from_message(message: str, normalized: str) -> list[str]:
    required: list[str] = []

    def add(scope: str) -> None:
        if scope not in required:
            required.append(scope)

    if _is_homemade_stock_consumption_request(normalized):
        add("homemade")
    if _is_inventory_consumption_estimation_request(normalized) and _has_explicit_inventory_estimation_scope(message, normalized):
        add("inventory")
        add("ordini")
    if _is_timeclock_request(normalized):
        add("timeclock")
    if _is_inventory_request(message, normalized):
        add("inventory")
    if _is_homemade_request(message, normalized):
        add("homemade")
    if _is_document_create_request(normalized):
        add("documents")
    if _is_fiscal_spend_request(message, normalized):
        add("fiscal_documents")
    if _is_fiscal_documents_request(normalized):
        add("fiscal_documents")
    if _is_prenotazioni_request(message, normalized):
        add("prenotazioni")
    if _is_menu_request(normalized):
        add("menu")
    if _is_orders_request(message, normalized):
        add("ordini")
    if _is_tips_request(message, normalized):
        if "sala" in normalized and "bar" not in normalized:
            add("tips_sala")
        elif "bar" in normalized and "sala" not in normalized:
            add("tips_bar")

    return required


def _assistant_missing_scopes_for_message(session: SessionIdentity, message: str, normalized: str) -> list[str]:
    required = _assistant_required_scopes_from_message(message, normalized)
    return [scope for scope in required if not _assistant_scope_allows(session, scope)]


def _assistant_required_scopes_for_tool_call(tool_call: PlannedToolCall) -> list[str]:
    tool_name = tool_call.tool
    if tool_name in _ORDERS_ASSISTANT_TOOLS:
        return ["ordini"]
    if tool_name in _PRENOTAZIONI_ASSISTANT_TOOLS:
        return ["prenotazioni"]
    if tool_name == "create_google_workspace_document":
        return ["documents"]
    if tool_name == "list_fiscal_documents":
        return ["fiscal_documents"]
    if tool_name == "get_timeclock_summary":
        return ["timeclock"]
    if tool_name == "get_inventory_consumption":
        return ["inventory"]
    if tool_name == "get_homemade_recipe":
        return ["homemade"]
    if tool_name == "run_tenant_query" and _sql_targets_homemade_stock(str(tool_call.arguments.get("sql") or "")):
        return ["homemade"]
    if tool_name == "get_module_settings" or tool_name == "update_module_settings":
        module_name = str(tool_call.arguments.get("module") or "").strip().lower()
        if module_name in {"prenotazioni", "whatsapp"}:
            return ["prenotazioni"]
        if module_name == "fiscal":
            return ["fiscal_documents"]
        if module_name == "ordini":
            return ["ordini"]
    return []


def _assistant_missing_scopes_for_tool_calls(session: SessionIdentity, tool_calls: list[PlannedToolCall]) -> list[str]:
    missing: list[str] = []
    for tool_call in tool_calls:
        for scope in _assistant_required_scopes_for_tool_call(tool_call):
            if scope not in missing and not _assistant_scope_allows(session, scope):
                missing.append(scope)
    return missing


def _extract_tips_date(message: str) -> date | None:
    lowered = message.casefold()
    today_local = _today_in_timezone()
    if "ieri" in lowered:
        return today_local - timedelta(days=1)
    if "oggi" in lowered or "stasera" in lowered or "questa sera" in lowered:
        return today_local

    iso_match = re.search(r"\b(20\d{2})-(\d{2})-(\d{2})\b", message)
    if iso_match:
        try:
            return date.fromisoformat(f"{iso_match.group(1)}-{iso_match.group(2)}-{iso_match.group(3)}")
        except ValueError:
            return None

    slash_match = re.search(r"\b(\d{1,2})/(\d{1,2})(?:/(\d{2,4}))?\b", message)
    if not slash_match:
        return None

    day_value = int(slash_match.group(1))
    month_value = int(slash_match.group(2))
    year_value = slash_match.group(3)
    if year_value is None:
        year = today_local.year
    elif len(year_value) == 2:
        year = 2000 + int(year_value)
    else:
        year = int(year_value)

    try:
        return date(year, month_value, day_value)
    except ValueError:
        return None


def _extract_tips_query(message: str) -> str:
    normalized = _normalize_text(message)
    if _is_tips_staff_breakdown_request(normalized) or _is_tips_report_request(normalized):
        return ""
    explicit_date = _extract_tips_date(message)
    date_tokens: set[str] = set()
    if explicit_date is not None:
        date_tokens.update(
            {
                str(explicit_date.day),
                f"{explicit_date.day:02d}",
                str(explicit_date.month),
                f"{explicit_date.month:02d}",
                str(explicit_date.year),
                str(explicit_date.year)[-2:],
            }
        )
    cleaned = re.sub(r"\b20\d{2}\b", " ", message, flags=re.IGNORECASE)
    cleaned = re.sub(r"\b\d{1,2}\s*[/-]\s*\d{1,2}(?:\s*[/-]\s*\d{2,4})?\b", " ", cleaned, flags=re.IGNORECASE)
    cleaned = re.sub(r"\b(?:nel|nella|su|sul|sulla)?\s*(?:mio|mia|tuo|tua)?\s*drive\b", " ", cleaned, flags=re.IGNORECASE)
    cleaned = re.sub(r"\b(?:nella|nel|su|sulla|sul)?\s*(?:cartella|folder)\s+.+$", " ", cleaned, flags=re.IGNORECASE)
    cleaned = re.sub(
        r"\b(?:mancia|mance|tips?|quanto|quanta|quanti|quante|totale|totali|storico|sorico|resoconto|riepilogo|report|rendiconto|prospetto|situazione|analisi|lista|elenco|tabella|salva|salvami|crea|creami|sheet|file|drive|cartella|folder|tutta|tutte|tutti|tutto|un|una|uno|il|lo|la|le|gli|i|mio|mia|miei|mie|ha\s+preso|hanno\s+preso|abbiamo\s+preso|preso|presa|prese|ha|hanno|abbiamo|nel|del|della|dei|delle|di|da|con|sala|bar)\b",
        " ",
        cleaned,
        flags=re.IGNORECASE,
    )
    tokens = [
        token
        for token in _tokenize_query(cleaned)
        if token
        and token not in _TIPS_PERSON_QUERY_IGNORED_TOKENS
        and token not in date_tokens
        and not re.fullmatch(r"20\d{2}", token)
    ]
    return " ".join(tokens[:6]).strip()


def _is_tips_staff_breakdown_request(normalized: str) -> bool:
    if any(fragment in normalized for fragment in _TIPS_STAFF_BREAKDOWN_FRAGMENTS):
        return True
    return _contains_normalized_word(normalized, "dipendente", "dipendenti", "staff", "ognuno", "ciascuno")


def _is_tips_report_request(normalized: str) -> bool:
    if any(fragment in normalized for fragment in _TIPS_REPORT_FRAGMENTS):
        return True
    if "manc" in normalized and _contains_normalized_word(normalized, "tutta", "tutte", "tutti", "tutto"):
        return True
    if "manc" in normalized and _contains_normalized_word(
        normalized,
        "dettagli",
        "dettagliata",
        "dettagliate",
        "dettagliati",
        "dettagliato",
        "dettaglio",
    ):
        return True
    return "manc" in normalized and _contains_normalized_word(
        normalized,
        "analisi",
        "elenco",
        "lista",
        "prospetto",
        "rendiconto",
        "report",
        "resoconto",
        "riepilogo",
        "situazione",
    )


def _extract_homemade_query(message: str) -> str:
    cleaned = re.sub(r"(\d+(?:[.,]\d+)?)\s*(?:lt|litri|litro|l|cl|ml)\b", " ", message, flags=re.IGNORECASE)
    for pattern in (
        r"\b(?:ricetta|prep|preparazione|homemade)\s+(?:di|del|della|dello|dei|delle)?\s*(.+)$",
        r"\b(?:come\s+faccio|come\s+si\s+fa|preparami|fammi|calcola(?:mi)?|quanto\s+mi\s+serve\s+per|quanto\s+serve\s+per|mi\s+dai)\s+(.+)$",
    ):
        match = re.search(pattern, cleaned, re.IGNORECASE)
        if match:
            cleaned = match.group(1)
            break
    cleaned = re.sub(r"^[\s:;,.!?-]+|[\s:;,.!?-]+$", "", cleaned)
    cleaned = re.sub(
        r"\b(?:ricetta|ricette|prep|preparazione|preparazioni|homemade|litri|litro|parti|proporzioni|proporzione|voglio|vorrei|fammi|preparami|dammi|mostrami|mostra|calcola|calcolami|come|faccio|si|fa|per|di|del|della|dei|delle)\b",
        " ",
        cleaned,
        flags=re.IGNORECASE,
    )
    cleaned = re.sub(r"\s+", " ", cleaned).strip(" .,!?:;")
    return cleaned


def _is_meaningful_homemade_query(value: str) -> bool:
    normalized = _normalize_text(value).strip()
    if not normalized:
        return False
    tokens = [token for token in normalized.split() if token]
    meaningful_tokens = [
        token
        for token in tokens
        if token not in {"e", "per", "di", "del", "della", "dei", "delle", "litri", "litro"}
        and not token.isdigit()
        and re.search(r"[a-z]", token)
    ]
    if not meaningful_tokens:
        return False
    return any(len(token) >= 3 for token in meaningful_tokens)


def _escape_sql_literal(value: str) -> str:
    return value.replace("'", "''")


def _flatten_settings_rows(module: str, payload: object, *, key_prefix: str = "") -> list[dict[str, object]]:
    rows: list[dict[str, object]] = []
    if isinstance(payload, dict):
        for key, value in payload.items():
            next_key = f"{key_prefix}.{key}" if key_prefix else str(key)
            rows.extend(_flatten_settings_rows(module, value, key_prefix=next_key))
        return rows
    if isinstance(payload, list):
        for index, value in enumerate(payload):
            next_key = f"{key_prefix}[{index}]"
            rows.extend(_flatten_settings_rows(module, value, key_prefix=next_key))
        return rows
    rows.append(
        {
            "module": module,
            "setting_key": key_prefix,
            "value_text": None if payload is None else str(payload),
            "value_json": json.dumps(payload, ensure_ascii=False),
        }
    )
    return rows


def _supplier_catalog_rows_with_aliases(rows: list[dict[str, object]]) -> list[dict[str, object]]:
    normalized_rows: list[dict[str, object]] = []
    for row in rows:
        normalized = dict(row)
        supplier_name = normalized.get("supplier_name")
        normalized.setdefault("name", supplier_name)
        normalized_rows.append(normalized)
    return normalized_rows


def _supplier_catalog_item_rows_with_aliases(rows: list[dict[str, object]]) -> list[dict[str, object]]:
    normalized_rows: list[dict[str, object]] = []
    for row in rows:
        normalized = dict(row)
        normalized.setdefault("product_name", normalized.get("source_name"))
        normalized.setdefault("lot_code", normalized.get("source_lot_code"))
        normalized.setdefault("supplier_name", normalized.get("source_supplier_name"))
        normalized.setdefault("price_vat", normalized.get("final_price_vat"))
        normalized.setdefault("supplier_catalog_id", normalized.get("catalog_id"))
        normalized_rows.append(normalized)
    return normalized_rows


async def _build_tenant_query_sandbox(session: SessionIdentity) -> sqlite3.Connection:
    connection = sqlite3.connect(":memory:")
    connection.row_factory = sqlite3.Row
    store = get_tenant_store()

    with store._connect_tenant_database(session.database_path):
        pass

    with _connect_orders_database_readonly(session) as orders_connection:
        _create_query_table_from_rows(connection, "tenant_profile", _rows_from_sqlite_table(orders_connection, "tenant_profile"))
        _create_query_table_from_rows(connection, "tenant_module_settings", _rows_from_sqlite_table(orders_connection, "tenant_module_settings"))

        if _tenant_query_allows_permission(session, "ordini"):
            product_rows = _rows_from_sqlite_table(orders_connection, "ordini_products")
            batch_rows = _rows_from_sqlite_table(orders_connection, "ordini_batches")
            item_rows = _rows_from_sqlite_table(orders_connection, "ordini_items")
            goal_rows = _rows_from_sqlite_table(orders_connection, "ordini_seasonal_goals")
            shared_note_rows = _rows_from_sqlite_table(orders_connection, "ordini_shared_notes")
            suspended_order_rows = _rows_from_sqlite_table(orders_connection, "ordini_suspended_orders")
            supplier_catalog_rows = _supplier_catalog_rows_with_aliases(_rows_from_sqlite_table(orders_connection, "supplier_catalogs"))
            supplier_catalog_item_rows = _supplier_catalog_item_rows_with_aliases(_rows_from_sqlite_table(orders_connection, "supplier_catalog_items"))
            _create_query_table_from_rows(connection, "ordini_products", product_rows)
            _create_query_table_from_rows(connection, "ordini_batches", batch_rows)
            _create_query_table_from_rows(connection, "ordini_items", item_rows)
            _create_query_table_from_rows(connection, "ordini_seasonal_goals", goal_rows)
            _create_query_table_from_rows(connection, "ordini_shared_notes", shared_note_rows)
            _create_query_table_from_rows(connection, "ordini_suspended_orders", suspended_order_rows)
            _create_query_table_from_rows(connection, "supplier_catalogs", supplier_catalog_rows)
            _create_query_table_from_rows(connection, "supplier_catalog_items", supplier_catalog_item_rows)
            _create_query_table_from_rows(connection, "products", product_rows)
            _create_query_table_from_rows(connection, "purchase_batches", batch_rows)
            _create_query_table_from_rows(connection, "purchase_items", item_rows)
            _create_query_table_from_rows(connection, "sales_goals", goal_rows)
            _create_query_table_from_rows(connection, "tenant_sales_goals", goal_rows)
            _create_query_table_from_rows(connection, "shared_notes", shared_note_rows)
            _create_query_table_from_rows(connection, "suspended_orders", suspended_order_rows)
            _create_query_table_from_rows(connection, "fornitori_cataloghi", supplier_catalog_rows)
            _create_query_table_from_rows(connection, "fornitori_cataloghi_items", supplier_catalog_item_rows)

        _ensure_query_table(
            connection,
            "ordini_products",
            [
                ("id", "INTEGER"),
                ("product_name", "TEXT"),
                ("lot_code", "TEXT"),
                ("supplier_name", "TEXT"),
            ],
        )
        _ensure_query_table(
            connection,
            "products",
            [
                ("id", "INTEGER"),
                ("product_name", "TEXT"),
                ("lot_code", "TEXT"),
                ("supplier_name", "TEXT"),
            ],
        )
        sales_goal_columns = [
            ("id", "INTEGER"),
            ("year", "INTEGER"),
            ("name", "TEXT"),
            ("goal_type", "TEXT"),
            ("description", "TEXT"),
            ("product_match", "TEXT"),
            ("secondary_product_match", "TEXT"),
            ("supplier_match", "TEXT"),
            ("target", "REAL"),
            ("secondary_target", "REAL"),
            ("unit_label", "TEXT"),
            ("bonus_label", "TEXT"),
        ]
        for table_name in ("ordini_seasonal_goals", "sales_goals", "tenant_sales_goals"):
            _ensure_query_table(connection, table_name, sales_goal_columns)

    locale_profile = _get_locale_profile(session)
    _create_query_table_from_rows(connection, "venue_profile", [locale_profile])
    _create_query_table_from_rows(connection, "profile", [locale_profile])

    if session.role in {"owner", "super_admin"}:
        user_rows = store.list_tenant_users(session)
        _create_query_table_from_rows(connection, "tenant_users", user_rows)
        _create_query_table_from_rows(connection, "users", user_rows)

    if _tenant_query_allows_permission(session, "timeclock"):
        reference_now = datetime.now(timezone.utc)
        timeclock_entries = []
        for entry in store._read_timeclock_entries(
            session,
            user_id=None if session.role in {"owner", "super_admin"} else session.user_id,
            limit=2000,
        ):
            serialized_entry = store._serialize_timeclock_entry(entry, reference_now=reference_now)
            timeclock_entries.append(
                {
                    **serialized_entry,
                    "staff_id": serialized_entry.get("user_id"),
                    "staff_name": serialized_entry.get("user_name") or serialized_entry.get("username") or serialized_entry.get("user_email"),
                    "employee_id": serialized_entry.get("user_id"),
                    "employee_name": serialized_entry.get("user_name") or serialized_entry.get("username") or serialized_entry.get("user_email"),
                    "hours": float(serialized_entry.get("duration_hours") or 0),
                }
            )
        active_timeclock_entries = [entry for entry in timeclock_entries if entry.get("ended_at") is None]
        _create_query_table_from_rows(connection, "tenant_timeclock_entries", timeclock_entries)
        _create_query_table_from_rows(connection, "timeclock_entries", timeclock_entries)
        _create_query_table_from_rows(connection, "timeclock", timeclock_entries)
        _create_query_table_from_rows(connection, "active_timeclock_entries", active_timeclock_entries)

    if _tenant_query_allows_permission(session, "inventory"):
        with _connect_orders_database_readonly(session) as inventory_connection:
            for table_name in (
                "tenant_inventory_warehouses",
                "tenant_inventory_stock_items",
                "tenant_inventory_stock_lots",
                "tenant_inventory_sessions",
                "tenant_inventory_session_items",
                "tenant_inventory_session_lots",
                "tenant_inventory_movements",
                "tenant_inventory_daily_consumptions",
                "tenant_inventory_estimated_consumptions",
                "tenant_inventory_consumption_product_stats",
                "tenant_inventory_snapshots",
                "tenant_inventory_snapshot_items",
                "tenant_inventory_snapshot_lots",
            ):
                _create_query_table_from_rows(connection, table_name, _rows_from_sqlite_table(inventory_connection, table_name))

        inventory_warehouse_rows: list[dict[str, object]] = []
        inventory_latest_item_rows: list[dict[str, object]] = []
        inventory_latest_lot_rows: list[dict[str, object]] = []
        inventory_payload = store.list_inventory_warehouses(session)
        warehouse_items = inventory_payload.get("warehouses") if isinstance(inventory_payload, dict) else []
        if isinstance(warehouse_items, list):
            for warehouse in warehouse_items:
                if not isinstance(warehouse, dict):
                    continue
                warehouse_id = str(warehouse.get("id") or "").strip()
                warehouse_name = str(warehouse.get("name") or "").strip()
                latest_inventory = warehouse.get("latest_inventory") if isinstance(warehouse.get("latest_inventory"), dict) else {}
                latest_session_id = str(latest_inventory.get("id") or "").strip()
                inventory_warehouse_rows.append(
                    {
                        "warehouse_id": warehouse_id,
                        "warehouse_name": warehouse_name,
                        "product_count": int(warehouse.get("product_count") or 0),
                        "current_total_equivalent_units": float(warehouse.get("total_equivalent_units") or 0),
                        "latest_inventory_date": warehouse.get("latest_inventory_date"),
                        "latest_inventory_session_id": latest_session_id or None,
                        "latest_inventory_label": latest_inventory.get("label"),
                        "latest_inventory_created_by_user_id": latest_inventory.get("created_by_user_id"),
                        "latest_inventory_created_by_name": latest_inventory.get("created_by_name"),
                        "latest_inventory_created_at": latest_inventory.get("created_at"),
                        "latest_inventory_updated_at": latest_inventory.get("updated_at"),
                        "latest_inventory_total_products": int(latest_inventory.get("total_products") or 0),
                        "latest_inventory_total_equivalent_units": float(latest_inventory.get("total_equivalent_units") or 0),
                        "inventory_session_count": int(warehouse.get("inventory_session_count") or 0),
                    }
                )
                if not warehouse_id:
                    continue
                if latest_session_id:
                    detail_payload = store.get_inventory_warehouse_session_detail(session, warehouse_id, latest_session_id)
                    session_payload = detail_payload.get("session") if isinstance(detail_payload.get("session"), dict) else {}
                    inventory_date = session_payload.get("inventory_date")
                    source_kind = "latest_inventory"
                else:
                    detail_payload = store.get_inventory_warehouse_detail(session, warehouse_id)
                    inventory_date = None
                    source_kind = "current_stock"
                session_items = detail_payload.get("items") if isinstance(detail_payload.get("items"), list) else []
                for item in session_items:
                    if not isinstance(item, dict):
                        continue
                    product_id = item.get("product_id")
                    inventory_latest_item_rows.append(
                        {
                            "warehouse_id": warehouse_id,
                            "warehouse_name": warehouse_name,
                            "session_id": latest_session_id or None,
                            "inventory_date": inventory_date,
                            "inventory_source": source_kind,
                            "product_id": int(product_id) if isinstance(product_id, (int, float)) else None,
                            "product_name": str(item.get("product_name") or ""),
                            "supplier_name": str(item.get("supplier_name") or ""),
                            "total_equivalent_units": float(item.get("total_equivalent_units") or 0),
                            "lot_count": int(item.get("lot_count") or 0),
                        }
                    )
                    for lot in item.get("lots") if isinstance(item.get("lots"), list) else []:
                        if not isinstance(lot, dict):
                            continue
                        inventory_latest_lot_rows.append(
                            {
                                "warehouse_id": warehouse_id,
                                "warehouse_name": warehouse_name,
                                "session_id": latest_session_id or None,
                                "inventory_date": inventory_date,
                                "inventory_source": source_kind,
                                "product_id": int(product_id) if isinstance(product_id, (int, float)) else None,
                                "product_name": str(item.get("product_name") or ""),
                                "supplier_name": str(item.get("supplier_name") or ""),
                                "lot_code": str(lot.get("lot_code") or ""),
                                "quantity": float(lot.get("quantity") or 0),
                                "units_per_pack": float(lot.get("units_per_pack")) if lot.get("units_per_pack") is not None else None,
                                "equivalent_units": float(lot.get("equivalent_units") or 0),
                            }
                        )

        _create_query_table_from_rows(connection, "inventory_warehouses", inventory_warehouse_rows)
        _create_query_table_from_rows(connection, "inventory_latest_items", inventory_latest_item_rows)
        _create_query_table_from_rows(connection, "inventory_latest_lots", inventory_latest_lot_rows)
        _ensure_query_table(
            connection,
            "inventory_warehouses",
            [
                ("warehouse_id", "TEXT"),
                ("warehouse_name", "TEXT"),
                ("product_count", "INTEGER"),
                ("current_total_equivalent_units", "REAL"),
                ("latest_inventory_date", "TEXT"),
                ("latest_inventory_session_id", "TEXT"),
                ("latest_inventory_label", "TEXT"),
                ("latest_inventory_created_by_user_id", "TEXT"),
                ("latest_inventory_created_by_name", "TEXT"),
                ("latest_inventory_created_at", "TEXT"),
                ("latest_inventory_updated_at", "TEXT"),
                ("latest_inventory_total_products", "INTEGER"),
                ("latest_inventory_total_equivalent_units", "REAL"),
                ("inventory_session_count", "INTEGER"),
            ],
        )
        _ensure_query_table(
            connection,
            "inventory_latest_items",
            [
                ("warehouse_id", "TEXT"),
                ("warehouse_name", "TEXT"),
                ("session_id", "TEXT"),
                ("inventory_date", "TEXT"),
                ("inventory_source", "TEXT"),
                ("product_id", "INTEGER"),
                ("product_name", "TEXT"),
                ("supplier_name", "TEXT"),
                ("total_equivalent_units", "REAL"),
                ("lot_count", "INTEGER"),
            ],
        )
        _ensure_query_table(
            connection,
            "inventory_latest_lots",
            [
                ("warehouse_id", "TEXT"),
                ("warehouse_name", "TEXT"),
                ("session_id", "TEXT"),
                ("inventory_date", "TEXT"),
                ("inventory_source", "TEXT"),
                ("product_id", "INTEGER"),
                ("product_name", "TEXT"),
                ("supplier_name", "TEXT"),
                ("lot_code", "TEXT"),
                ("quantity", "REAL"),
                ("units_per_pack", "REAL"),
                ("equivalent_units", "REAL"),
            ],
        )

    if _tenant_query_allows_permission(session, "homemade"):
        with _connect_orders_database_readonly(session) as homemade_connection:
            homemade_recipe_rows = _rows_from_sqlite_table(homemade_connection, "tenant_homemade_recipes")
            homemade_stock_warehouse_rows = _rows_from_sqlite_table(homemade_connection, "tenant_homemade_stock_warehouses")
            homemade_stock_item_rows = _rows_from_sqlite_table(homemade_connection, "tenant_homemade_stock_items")
            homemade_stock_movement_rows = _rows_from_sqlite_table(homemade_connection, "tenant_homemade_stock_movements")
            homemade_stock_settings_rows = _rows_from_sqlite_table(homemade_connection, "tenant_homemade_stock_settings")

        for table_name, rows in (
            ("tenant_homemade_recipes", homemade_recipe_rows),
            ("homemade_recipes", homemade_recipe_rows),
            ("prep_recipes", homemade_recipe_rows),
            ("tenant_homemade_stock_warehouses", homemade_stock_warehouse_rows),
            ("homemade_stock_warehouses", homemade_stock_warehouse_rows),
            ("prep_stock_warehouses", homemade_stock_warehouse_rows),
            ("tenant_homemade_stock_items", homemade_stock_item_rows),
            ("homemade_stock_items", homemade_stock_item_rows),
            ("prep_stock_items", homemade_stock_item_rows),
            ("tenant_homemade_stock_movements", homemade_stock_movement_rows),
            ("homemade_stock_movements", homemade_stock_movement_rows),
            ("prep_stock_movements", homemade_stock_movement_rows),
            ("tenant_homemade_stock_settings", homemade_stock_settings_rows),
            ("homemade_stock_settings", homemade_stock_settings_rows),
            ("prep_stock_settings", homemade_stock_settings_rows),
        ):
            _create_query_table_from_rows(connection, table_name, rows)

        homemade_operational_day_rows = _homemade_operational_day_rows(homemade_stock_settings_rows)
        for table_name in ("tenant_homemade_operational_days", "homemade_operational_days", "prep_operational_days"):
            _create_query_table_from_rows(connection, table_name, homemade_operational_day_rows)

        homemade_recipe_columns = [
            ("id", "TEXT"),
            ("name", "TEXT"),
            ("name_lookup", "TEXT"),
            ("measurement_unit", "TEXT"),
            ("notes", "TEXT"),
            ("total_parts", "REAL"),
            ("ingredient_count", "INTEGER"),
            ("created_by_user_id", "TEXT"),
            ("created_by_name", "TEXT"),
            ("created_at", "TEXT"),
            ("updated_at", "TEXT"),
            ("preparation_date", "TEXT"),
            ("yield_ingredient_name", "TEXT"),
            ("yield_ingredient_lookup", "TEXT"),
            ("yield_input_amount", "REAL"),
            ("yield_input_unit", "TEXT"),
            ("yield_output_ml", "REAL"),
            ("usage_scope", "TEXT"),
        ]
        homemade_stock_warehouse_columns = [
            ("id", "TEXT"),
            ("name", "TEXT"),
            ("created_at", "TEXT"),
            ("updated_at", "TEXT"),
        ]
        homemade_stock_item_columns = [
            ("id", "TEXT"),
            ("warehouse_id", "TEXT"),
            ("recipe_id", "TEXT"),
            ("recipe_name", "TEXT"),
            ("recipe_lookup", "TEXT"),
            ("measurement_unit", "TEXT"),
            ("quantity", "REAL"),
            ("created_at", "TEXT"),
            ("updated_at", "TEXT"),
        ]
        homemade_stock_movement_columns = [
            ("id", "TEXT"),
            ("warehouse_id", "TEXT"),
            ("warehouse_name", "TEXT"),
            ("recipe_id", "TEXT"),
            ("recipe_name", "TEXT"),
            ("recipe_lookup", "TEXT"),
            ("measurement_unit", "TEXT"),
            ("quantity_before", "REAL"),
            ("quantity_after", "REAL"),
            ("delta_quantity", "REAL"),
            ("added_quantity", "REAL"),
            ("consumed_quantity", "REAL"),
            ("movement_type", "TEXT"),
            ("occurred_at", "TEXT"),
            ("created_by_user_id", "TEXT"),
            ("created_by_name", "TEXT"),
            ("created_at", "TEXT"),
        ]
        homemade_stock_settings_columns = [
            ("id", "TEXT"),
            ("minimum_stock_days", "REAL"),
            ("created_at", "TEXT"),
            ("updated_at", "TEXT"),
            ("bar_calendar_json", "TEXT"),
            ("restaurant_calendar_json", "TEXT"),
        ]
        homemade_operational_day_columns = [
            ("usage_scope", "TEXT"),
            ("operational_date", "TEXT"),
        ]
        for table_name in ("tenant_homemade_recipes", "homemade_recipes", "prep_recipes"):
            _ensure_query_table(connection, table_name, homemade_recipe_columns)
        for table_name in ("tenant_homemade_stock_warehouses", "homemade_stock_warehouses", "prep_stock_warehouses"):
            _ensure_query_table(connection, table_name, homemade_stock_warehouse_columns)
        for table_name in ("tenant_homemade_stock_items", "homemade_stock_items", "prep_stock_items"):
            _ensure_query_table(connection, table_name, homemade_stock_item_columns)
        for table_name in ("tenant_homemade_stock_movements", "homemade_stock_movements", "prep_stock_movements"):
            _ensure_query_table(connection, table_name, homemade_stock_movement_columns)
        for table_name in ("tenant_homemade_stock_settings", "homemade_stock_settings", "prep_stock_settings"):
            _ensure_query_table(connection, table_name, homemade_stock_settings_columns)
        for table_name in ("tenant_homemade_operational_days", "homemade_operational_days", "prep_operational_days"):
            _ensure_query_table(connection, table_name, homemade_operational_day_columns)

    allowed_tips_areas = _tenant_query_allowed_tips_areas(session)
    if allowed_tips_areas:
        with _connect_orders_database_readonly(session) as tips_connection:
            tips_roster_rows = [
                row
                for row in _rows_from_sqlite_table(tips_connection, "tenant_tips_roster_entries")
                if str(row.get("area") or "").strip() in allowed_tips_areas
            ]
            tips_run_rows = [
                row
                for row in _rows_from_sqlite_table(tips_connection, "tenant_tips_runs")
                if str(row.get("area") or "").strip() in allowed_tips_areas
            ]
            allowed_run_ids = {str(row.get("id") or "").strip() for row in tips_run_rows if str(row.get("id") or "").strip()}
            tips_entry_rows = [
                row
                for row in _rows_from_sqlite_table(tips_connection, "tenant_tips_run_entries")
                if str(row.get("area") or "").strip() in allowed_tips_areas and str(row.get("run_id") or "").strip() in allowed_run_ids
            ]
            tips_history_rows = [
                row
                for row in _rows_from_sqlite_table(tips_connection, "tenant_tips_run_history_sources")
                if str(row.get("run_id") or "").strip() in allowed_run_ids
            ]

        _create_query_table_from_rows(connection, "tenant_tips_roster_entries", tips_roster_rows)
        _create_query_table_from_rows(connection, "tenant_tips_runs", tips_run_rows)
        _create_query_table_from_rows(connection, "tenant_tips_run_entries", tips_entry_rows)
        _create_query_table_from_rows(connection, "tenant_tips_run_history_sources", tips_history_rows)
        _create_query_table_from_rows(connection, "tips_roster", tips_roster_rows)
        _create_query_table_from_rows(connection, "tips_runs", tips_run_rows)
        _create_query_table_from_rows(connection, "tips_run_entries", tips_entry_rows)
        _create_query_table_from_rows(connection, "tips_history_sources", tips_history_rows)
        _create_query_table_from_rows(connection, "mance_roster", tips_roster_rows)
        _create_query_table_from_rows(connection, "mance_runs", tips_run_rows)
        _create_query_table_from_rows(connection, "mance_entries", tips_entry_rows)
        _create_query_table_from_rows(connection, "mance_history_sources", tips_history_rows)

        _ensure_query_table(
            connection,
            "tenant_tips_runs",
            [
                ("id", "TEXT"),
                ("area", "TEXT"),
                ("tip_date", "TEXT"),
                ("total_tip_amount", "REAL"),
                ("tip_pos_amount", "REAL"),
                ("tip_pos_effective_amount", "REAL"),
                ("tip_cash_amount", "REAL"),
                ("total_score", "REAL"),
                ("historical_total_amount", "REAL"),
                ("payable_total_amount", "REAL"),
                ("present_staff_count", "INTEGER"),
                ("absent_staff_count", "INTEGER"),
                ("payout_status", "TEXT"),
                ("settled_at", "TEXT"),
                ("settled_by_name", "TEXT"),
                ("saved_by_user_id", "TEXT"),
                ("saved_by_name", "TEXT"),
                ("created_at", "TEXT"),
                ("updated_at", "TEXT"),
            ],
        )
        _ensure_query_table(
            connection,
            "tenant_tips_run_entries",
            [
                ("id", "TEXT"),
                ("run_id", "TEXT"),
                ("area", "TEXT"),
                ("staff_name", "TEXT"),
                ("staff_lookup", "TEXT"),
                ("score", "REAL"),
                ("is_present", "INTEGER"),
                ("amount_today", "REAL"),
                ("historical_amount", "REAL"),
                ("total_amount", "REAL"),
                ("created_at", "TEXT"),
                ("updated_at", "TEXT"),
            ],
        )
        _ensure_query_table(
            connection,
            "tenant_tips_roster_entries",
            [
                ("id", "TEXT"),
                ("area", "TEXT"),
                ("staff_name", "TEXT"),
                ("staff_lookup", "TEXT"),
                ("score", "REAL"),
                ("sort_order", "INTEGER"),
                ("created_at", "TEXT"),
                ("updated_at", "TEXT"),
            ],
        )
        _ensure_query_table(
            connection,
            "tenant_tips_run_history_sources",
            [
                ("id", "TEXT"),
                ("run_id", "TEXT"),
                ("source_run_id", "TEXT"),
                ("source_tip_date", "TEXT"),
                ("created_at", "TEXT"),
                ("updated_at", "TEXT"),
            ],
        )
        _ensure_query_table(
            connection,
            "tips_runs",
            [
                ("id", "TEXT"),
                ("area", "TEXT"),
                ("tip_date", "TEXT"),
                ("total_tip_amount", "REAL"),
                ("tip_pos_amount", "REAL"),
                ("tip_pos_effective_amount", "REAL"),
                ("tip_cash_amount", "REAL"),
                ("total_score", "REAL"),
                ("historical_total_amount", "REAL"),
                ("payable_total_amount", "REAL"),
                ("present_staff_count", "INTEGER"),
                ("absent_staff_count", "INTEGER"),
                ("payout_status", "TEXT"),
                ("settled_at", "TEXT"),
                ("settled_by_name", "TEXT"),
                ("saved_by_user_id", "TEXT"),
                ("saved_by_name", "TEXT"),
                ("created_at", "TEXT"),
                ("updated_at", "TEXT"),
            ],
        )
        _ensure_query_table(
            connection,
            "tips_run_entries",
            [
                ("id", "TEXT"),
                ("run_id", "TEXT"),
                ("area", "TEXT"),
                ("staff_name", "TEXT"),
                ("staff_lookup", "TEXT"),
                ("score", "REAL"),
                ("is_present", "INTEGER"),
                ("amount_today", "REAL"),
                ("historical_amount", "REAL"),
                ("total_amount", "REAL"),
                ("created_at", "TEXT"),
                ("updated_at", "TEXT"),
            ],
        )
        _ensure_query_table(
            connection,
            "tips_roster",
            [
                ("id", "TEXT"),
                ("area", "TEXT"),
                ("staff_name", "TEXT"),
                ("staff_lookup", "TEXT"),
                ("score", "REAL"),
                ("sort_order", "INTEGER"),
                ("created_at", "TEXT"),
                ("updated_at", "TEXT"),
            ],
        )
        _ensure_query_table(
            connection,
            "tips_history_sources",
            [
                ("id", "TEXT"),
                ("run_id", "TEXT"),
                ("source_run_id", "TEXT"),
                ("source_tip_date", "TEXT"),
                ("created_at", "TEXT"),
                ("updated_at", "TEXT"),
            ],
        )
        _ensure_query_table(
            connection,
            "mance_runs",
            [
                ("id", "TEXT"),
                ("area", "TEXT"),
                ("tip_date", "TEXT"),
                ("total_tip_amount", "REAL"),
                ("tip_pos_amount", "REAL"),
                ("tip_pos_effective_amount", "REAL"),
                ("tip_cash_amount", "REAL"),
                ("total_score", "REAL"),
                ("historical_total_amount", "REAL"),
                ("payable_total_amount", "REAL"),
                ("present_staff_count", "INTEGER"),
                ("absent_staff_count", "INTEGER"),
                ("payout_status", "TEXT"),
                ("settled_at", "TEXT"),
                ("settled_by_name", "TEXT"),
                ("saved_by_user_id", "TEXT"),
                ("saved_by_name", "TEXT"),
                ("created_at", "TEXT"),
                ("updated_at", "TEXT"),
            ],
        )
        _ensure_query_table(
            connection,
            "mance_entries",
            [
                ("id", "TEXT"),
                ("run_id", "TEXT"),
                ("area", "TEXT"),
                ("staff_name", "TEXT"),
                ("staff_lookup", "TEXT"),
                ("score", "REAL"),
                ("is_present", "INTEGER"),
                ("amount_today", "REAL"),
                ("historical_amount", "REAL"),
                ("total_amount", "REAL"),
                ("created_at", "TEXT"),
                ("updated_at", "TEXT"),
            ],
        )
        _ensure_query_table(
            connection,
            "mance_roster",
            [
                ("id", "TEXT"),
                ("area", "TEXT"),
                ("staff_name", "TEXT"),
                ("staff_lookup", "TEXT"),
                ("score", "REAL"),
                ("sort_order", "INTEGER"),
                ("created_at", "TEXT"),
                ("updated_at", "TEXT"),
            ],
        )
        _ensure_query_table(
            connection,
            "mance_history_sources",
            [
                ("id", "TEXT"),
                ("run_id", "TEXT"),
                ("source_run_id", "TEXT"),
                ("source_tip_date", "TEXT"),
                ("created_at", "TEXT"),
                ("updated_at", "TEXT"),
            ],
        )

    if _tenant_query_allows_permission(session, "fiscal_documents"):
        fiscal_documents = [
            {
                "id": record.id,
                "display_name": record.display_name,
                "document_type": record.document_type,
                "document_number": record.document_number,
                "document_date": record.document_date,
                "supplier_name": record.supplier_name,
                "total_amount": record.total_amount,
                "currency": record.currency,
                "status": record.status,
                "matching_status": record.matching_status,
                "review_status": record.review_status,
                "summary_text": record.summary_text,
                "created_at": record.created_at,
                "updated_at": record.updated_at,
            }
            for record in store.list_fiscal_documents(session.tenant_id)
        ]
        _create_query_table_from_rows(connection, "tenant_fiscal_documents", fiscal_documents)
        _create_query_table_from_rows(connection, "fiscal_documents", fiscal_documents)

        fiscal_document_items = [
            {
                "id": item.id,
                "document_id": item.document_id,
                "line_index": item.line_index,
                "product_code": item.product_code,
                "iso_code": item.iso_code,
                "description": item.description,
                "category_code": item.category_code,
                "unit_code": item.unit_code,
                "pack_count": item.pack_count,
                "quantity": item.quantity,
                "gross_quantity": item.gross_quantity,
                "tare_quantity": item.tare_quantity,
                "net_quantity": item.net_quantity,
                "unit_price": item.unit_price,
                "line_total": item.line_total,
                "vat_code": item.vat_code,
                "raw_row_text": item.raw_row_text,
                "created_at": item.created_at,
                "updated_at": item.updated_at,
            }
            for document in store.list_fiscal_documents(session.tenant_id)
            for item in store.list_fiscal_document_items(session.tenant_id, document.id)
        ]
        _create_query_table_from_rows(connection, "tenant_fiscal_document_items", fiscal_document_items)
        _create_query_table_from_rows(connection, "fiscal_document_items", fiscal_document_items)

        fiscal_inbox_items = [
            {
                "id": record.id,
                "message_id": record.message_id,
                "attachment_id": record.attachment_id,
                "subject": record.subject,
                "sender": record.sender,
                "received_at": record.received_at,
                "attachment_name": record.attachment_name,
                "mime_type": record.mime_type,
                "sync_status": record.sync_status,
                "document_id": record.document_id,
                "error_detail": record.error_detail,
                "created_at": record.created_at,
                "updated_at": record.updated_at,
            }
            for record in store.list_fiscal_document_inbox_items(session.tenant_id)
        ]
        _create_query_table_from_rows(connection, "tenant_fiscal_document_inbox_items", fiscal_inbox_items)
        _create_query_table_from_rows(connection, "fiscal_inbox_items", fiscal_inbox_items)

        fiscal_settings = store.get_fiscal_document_settings(session.tenant_id)
        _create_query_table_from_rows(
            connection,
            "tenant_fiscal_document_settings",
            [
                {
                    "tenant_id": fiscal_settings.tenant_id,
                    "inbound_email": fiscal_settings.inbound_email,
                    "updated_at": fiscal_settings.updated_at,
                }
            ],
        )
        _create_query_table_from_rows(
            connection,
            "fiscal_settings",
            [
                {
                    "tenant_id": fiscal_settings.tenant_id,
                    "inbound_email": fiscal_settings.inbound_email,
                    "updated_at": fiscal_settings.updated_at,
                }
            ],
        )

    if _tenant_query_allows_permission(session, "menu"):
        menu_assets = [
            {
                "id": asset.id,
                "display_name": asset.display_name,
                "original_name": asset.original_name,
                "mime_type": asset.mime_type,
                "kind": asset.kind,
                "file_size_bytes": asset.file_size_bytes,
                "status": asset.status,
                "created_at": asset.created_at,
                "updated_at": asset.updated_at,
            }
            for asset in store.list_menu_assets(session.tenant_id)
        ]
        _create_query_table_from_rows(connection, "tenant_menu_assets", menu_assets)
        _create_query_table_from_rows(connection, "menu_assets", menu_assets)

    module_rows: list[dict[str, object]] = []
    if _tenant_query_allows_permission(session, "ordini"):
        try:
            ordini_settings = await _get_module_settings(session, GetModuleSettingsArgs(module="ordini"))
            module_rows.extend(_flatten_settings_rows("ordini", ordini_settings))
        except HTTPException:
            logger.warning(
                "tenant_query_sandbox_ordini_settings_unavailable",
                extra={"tenant_id": session.tenant_id, "tenant_slug": session.tenant_slug},
            )
    if _tenant_query_allows_permission(session, "prenotazioni"):
        try:
            prenotazioni_settings = await _get_module_settings(session, GetModuleSettingsArgs(module="prenotazioni"))
            module_rows.extend(_flatten_settings_rows("prenotazioni", prenotazioni_settings))
        except HTTPException:
            logger.warning(
                "tenant_query_sandbox_prenotazioni_settings_unavailable",
                extra={"tenant_id": session.tenant_id, "tenant_slug": session.tenant_slug},
            )
        try:
            whatsapp_settings = await _get_module_settings(session, GetModuleSettingsArgs(module="whatsapp"))
            module_rows.extend(_flatten_settings_rows("whatsapp", whatsapp_settings))
        except HTTPException:
            logger.warning(
                "tenant_query_sandbox_whatsapp_settings_unavailable",
                extra={"tenant_id": session.tenant_id, "tenant_slug": session.tenant_slug},
            )
        try:
            reservations_payload = await _list_reservations_full(session, ListReservationsArgs(limit=_CHAT_LIST_LIMIT))
        except HTTPException:
            reservations_payload = {}
            logger.warning(
                "tenant_query_sandbox_reservations_unavailable",
                extra={"tenant_id": session.tenant_id, "tenant_slug": session.tenant_slug},
            )
        reservations = reservations_payload.get("reservations") if isinstance(reservations_payload, dict) else []
        if isinstance(reservations, list):
            reservation_rows = [{str(key): value for key, value in item.items()} for item in reservations if isinstance(item, dict)]
            _create_query_table_from_rows(connection, "tenant_reservations", reservation_rows)
            _create_query_table_from_rows(connection, "reservations", reservation_rows)
    if session.role in {"owner", "super_admin"}:
        try:
            llm_settings = await _get_module_settings(session, GetModuleSettingsArgs(module="llm"))
            module_rows.extend(_flatten_settings_rows("llm", llm_settings))
        except HTTPException:
            logger.warning(
                "tenant_query_sandbox_llm_settings_unavailable",
                extra={"tenant_id": session.tenant_id, "tenant_slug": session.tenant_slug},
            )
    if _tenant_query_allows_permission(session, "fiscal_documents"):
        try:
            fiscal_module_settings = await _get_module_settings(session, GetModuleSettingsArgs(module="fiscal"))
            module_rows.extend(_flatten_settings_rows("fiscal", fiscal_module_settings))
        except HTTPException:
            logger.warning(
                "tenant_query_sandbox_fiscal_settings_unavailable",
                extra={"tenant_id": session.tenant_id, "tenant_slug": session.tenant_slug},
            )
    _create_query_table_from_rows(connection, "tenant_runtime_settings", module_rows)
    _create_query_table_from_rows(connection, "settings", module_rows)

    connection.execute("PRAGMA query_only = ON;")
    return connection


def _describe_sqlite_schema(connection: sqlite3.Connection) -> dict[str, object]:
    table_rows = connection.execute(
        "SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name ASC"
    ).fetchall()
    tables: list[dict[str, object]] = []
    for row in table_rows:
        table_name = str(row["name"])
        columns = [
            {
                "name": str(column["name"]),
                "type": str(column["type"] or "TEXT"),
            }
            for column in connection.execute(f"PRAGMA table_info({_quote_sqlite_ident(table_name)})").fetchall()
        ]
        total_row = connection.execute(f"SELECT COUNT(*) AS total FROM {_quote_sqlite_ident(table_name)}").fetchone()
        tables.append(
            {
                "table": table_name,
                "row_count": int(total_row["total"] or 0) if total_row is not None else 0,
                "columns": columns,
            }
        )
    return {
        "tables": tables,
        "notes": [
            "Usa solo SELECT o WITH ... SELECT.",
            "Le tabelle disponibili dipendono dai permessi del tenant corrente.",
            "Per risultati grandi applica filtri, ORDER BY e LIMIT.",
            "I cataloghi fornitori salvati separatamente dal catalogo prodotti del locale sono nelle tabelle supplier_catalogs e supplier_catalog_items.",
            "Per bolle e fatture usa tenant_fiscal_documents per la testata documento e tenant_fiscal_document_items per le righe merce estratte.",
            "Per mance usa tenant_tips_runs per le giornate salvate, tenant_tips_run_entries per il dettaglio dipendente e tenant_tips_roster_entries per la lista staff configurata.",
            "Per consumi delle prep/homemade usa tenant_homemade_stock_movements; per stock attuale prep usa tenant_homemade_stock_items.",
            "Per medie giornaliere delle prep usa tenant_homemade_operational_days come calendario operativo bar/ristorante.",
        ],
        "example_queries": [
            "SELECT supplier_name, COUNT(*) AS products FROM ordini_products GROUP BY supplier_name ORDER BY products DESC LIMIT 10",
            "SELECT items.source_supplier_name, items.source_name, items.final_price_vat FROM supplier_catalog_items AS items WHERE lower(items.source_name) LIKE '%vodka%' ORDER BY items.final_price_vat ASC LIMIT 20",
            "SELECT items.source_name, GROUP_CONCAT(DISTINCT items.source_supplier_name) AS suppliers FROM supplier_catalog_items AS items WHERE lower(items.source_name) LIKE '%cointreau%' GROUP BY lower(items.source_name) LIMIT 20",
            "SELECT document_type, supplier_name, total_amount FROM tenant_fiscal_documents ORDER BY document_date DESC LIMIT 20",
            "SELECT d.document_date, d.supplier_name, i.description, COALESCE(i.net_quantity, i.quantity, i.pack_count) AS delivered_quantity, i.unit_code, i.line_total FROM tenant_fiscal_documents AS d JOIN tenant_fiscal_document_items AS i ON i.document_id = d.id WHERE d.document_type = 'delivery_note' ORDER BY d.document_date DESC, i.line_index ASC LIMIT 50",
            "SELECT reservation_date, start_time, customer_name, guests FROM tenant_reservations ORDER BY reservation_date DESC, start_time DESC LIMIT 20",
            "SELECT user_name, SUM(COALESCE(duration_seconds, 0)) AS total_seconds FROM tenant_timeclock_entries GROUP BY user_id ORDER BY total_seconds DESC LIMIT 20",
            "SELECT e.staff_name, SUM(COALESCE(e.amount_today, 0)) AS total_mance_giornata FROM tenant_tips_run_entries AS e JOIN tenant_tips_runs AS r ON r.id = e.run_id WHERE substr(r.tip_date, 1, 4) = '2025' GROUP BY e.staff_lookup, e.staff_name ORDER BY total_mance_giornata DESC LIMIT 20",
            "SELECT movements.recipe_name, SUM(movements.consumed_quantity) AS consumed_pz, COUNT(DISTINCT days.operational_date) AS giorni_operativi FROM tenant_homemade_stock_movements AS movements LEFT JOIN tenant_homemade_recipes AS recipes ON recipes.id = movements.recipe_id LEFT JOIN tenant_homemade_operational_days AS days ON days.usage_scope IN (COALESCE(recipes.usage_scope, 'both'), CASE WHEN COALESCE(recipes.usage_scope, 'both') = 'both' THEN 'bar' ELSE COALESCE(recipes.usage_scope, 'both') END, CASE WHEN COALESCE(recipes.usage_scope, 'both') = 'both' THEN 'restaurant' ELSE COALESCE(recipes.usage_scope, 'both') END) WHERE movements.consumed_quantity > 0 GROUP BY movements.recipe_lookup, movements.recipe_name ORDER BY consumed_pz DESC LIMIT 20",
        ],
    }


def _normalize_sql_readonly_query(sql: str) -> str:
    cleaned = re.sub(r"/\*.*?\*/", " ", sql, flags=re.DOTALL)
    cleaned = re.sub(r"--[^\n\r]*", " ", cleaned)
    cleaned = cleaned.strip().rstrip(";").strip()
    cleaned = re.sub(r"\bILIKE\b", "LIKE", cleaned, flags=re.IGNORECASE)
    compatibility_replacements = {
        r"\bsci\.product_name\b": "sci.source_name",
        r"\bsci\.lot_code\b": "sci.source_lot_code",
        r"\bsci\.price_vat\b": "sci.final_price_vat",
        r"\bsci\.supplier_catalog_id\b": "sci.catalog_id",
        r"\bsc\.name\b": "sc.supplier_name",
    }
    for pattern, replacement in compatibility_replacements.items():
        cleaned = re.sub(pattern, replacement, cleaned, flags=re.IGNORECASE)
    if re.search(r"\b(?:supplier_catalog_items|fornitori_cataloghi_items)\b", cleaned, flags=re.IGNORECASE):
        supplier_catalog_column_aliases = {
            r"(?<!\.)\bproduct_name\b": "source_name",
            r"(?<!\.)\blot_code\b": "source_lot_code",
            r"(?<!\.)\bsupplier_name\b": "source_supplier_name",
            r"(?<!\.)\bprice_vat\b": "final_price_vat",
            r"(?<!\.)\bsupplier_catalog_id\b": "catalog_id",
        }
        for pattern, replacement in supplier_catalog_column_aliases.items():
            cleaned = re.sub(pattern, replacement, cleaned, flags=re.IGNORECASE)
    if not cleaned:
        raise HTTPException(status_code=400, detail="Query SQL vuota.")
    if ";" in cleaned:
        raise HTTPException(status_code=400, detail="E' consentita una sola query SQL per volta.")
    if not re.match(r"(?is)^(select|with)\b", cleaned):
        raise HTTPException(status_code=400, detail="Sono consentite solo query SELECT.")
    if _TENANT_QUERY_DISALLOWED_PATTERN.search(cleaned):
        raise HTTPException(status_code=400, detail="La query contiene istruzioni SQL non consentite.")
    return cleaned


def _tenant_query_authorizer(allowed_tables: set[str]):
    sqlite_ok = sqlite3.SQLITE_OK
    sqlite_deny = sqlite3.SQLITE_DENY
    allowed_actions = {
        sqlite3.SQLITE_SELECT,
        sqlite3.SQLITE_READ,
    }
    function_code = getattr(sqlite3, "SQLITE_FUNCTION", None)
    if function_code is not None:
        allowed_actions.add(function_code)

    def authorizer(action_code: int, arg1: str | None, arg2: str | None, db_name: str | None, trigger_name: str | None) -> int:
        if action_code == sqlite3.SQLITE_READ:
            table_name = str(arg1 or "")
            return sqlite_ok if table_name in allowed_tables or table_name == "sqlite_master" else sqlite_deny
        return sqlite_ok if action_code in allowed_actions else sqlite_deny

    return authorizer


async def _describe_tenant_schema_tool(session: SessionIdentity, args: DescribeTenantSchemaArgs) -> dict[str, object]:
    sandbox = await _build_tenant_query_sandbox(session)
    try:
        schema = _describe_sqlite_schema(sandbox)
        if not args.include_examples:
            schema.pop("example_queries", None)
        return schema
    finally:
        sandbox.close()


async def _run_tenant_query_tool(session: SessionIdentity, args: RunTenantQueryArgs) -> dict[str, object]:
    sql = _normalize_sql_readonly_query(args.sql)
    sandbox = await _build_tenant_query_sandbox(session)
    try:
        allowed_tables = {
            str(row["name"])
            for row in sandbox.execute(
                "SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'"
            ).fetchall()
        }
        sandbox.set_authorizer(_tenant_query_authorizer(allowed_tables))

        deadline = monotonic() + _TENANT_QUERY_TIMEOUT_SECONDS

        def _progress_handler() -> int:
            return 1 if monotonic() > deadline else 0

        sandbox.set_progress_handler(_progress_handler, 1000)
        cursor = sandbox.execute(sql)
        columns = [str(item[0]) for item in (cursor.description or [])]
        raw_rows = cursor.fetchmany(args.limit + 1)
        truncated = len(raw_rows) > args.limit
        rows = raw_rows[: args.limit]
        rendered_rows = [
            {columns[index]: _json_safe_cell(row[index]) for index in range(len(columns))}
            for row in rows
        ]
        logger.info(
            "tenant_query tenant=%s user=%s rows=%s truncated=%s sql=%s",
            session.tenant_slug,
            session.user_email,
            len(rendered_rows),
            truncated,
            sql,
        )
        return {
            "sql": sql,
            "columns": columns,
            "rows": rendered_rows,
            "row_count": len(rendered_rows),
            "truncated": truncated,
            "limit": args.limit,
            "available_tables": sorted(allowed_tables),
        }
    except sqlite3.OperationalError as exc:
        detail = "Query interrotta per timeout." if "interrupted" in str(exc).lower() else f"Errore SQL: {exc}"
        raise HTTPException(status_code=400, detail=detail) from exc
    finally:
        sandbox.close()


def _orders_table_columns(connection: sqlite3.Connection, table_name: str) -> set[str]:
    rows = connection.execute(f"PRAGMA table_info({table_name})").fetchall()
    return {str(row["name"]) for row in rows}


def _row_get(row: sqlite3.Row | None, key: str) -> object | None:
    if row is None:
        return None
    try:
        return row[key]
    except Exception:
        return None


def _get_locale_profile(session: SessionIdentity) -> dict[str, object]:
    tenant_context = get_tenant_store().get_tenant_context(session.tenant_id)
    venue = tenant_context["venues"][0] if tenant_context["venues"] else None

    with _connect_orders_database(session) as connection:
        tenant_profile = connection.execute("SELECT * FROM tenant_profile LIMIT 1").fetchone()
        module_rows = list(connection.execute("SELECT * FROM tenant_module_settings ORDER BY enabled DESC, module_key ASC"))

    return {
        "tenant_name": tenant_context["tenant"].name,
        "tenant_slug": tenant_context["tenant"].slug,
        "venue_name": venue.name if venue is not None else tenant_context["tenant"].name,
        "address": getattr(venue, "address", "") or "",
        "phone_number": getattr(venue, "phone_number", None),
        "whatsapp_number": getattr(venue, "whatsapp_number", None),
        "admin_name": tenant_profile["admin_name"] if tenant_profile is not None else session.user_name,
        "admin_email": tenant_profile["admin_email"] if tenant_profile is not None else session.user_email,
        "enabled_modules": [row["module_key"] for row in module_rows if int(row["enabled"]) == 1],
    }


def _load_product_candidates(session: SessionIdentity) -> list[ProductCandidate]:
    with _connect_orders_database(session) as connection:
        product_columns = _orders_table_columns(connection, "ordini_products")
        select_product_code = "products.product_code" if "product_code" in product_columns else "NULL AS product_code"
        select_final_price_vat = "products.final_price_vat" if "final_price_vat" in product_columns else "NULL AS final_price_vat"
        select_weight_kg = "products.weight_kg" if "weight_kg" in product_columns else "NULL AS weight_kg"
        select_unit_price_per_kg = "products.unit_price_per_kg" if "unit_price_per_kg" in product_columns else "NULL AS unit_price_per_kg"
        select_category = "products.category" if "category" in product_columns else "NULL AS category"
        select_units_per_pack = "products.units_per_pack" if "units_per_pack" in product_columns else "NULL AS units_per_pack"
        select_liters_per_unit = "products.liters_per_unit" if "liters_per_unit" in product_columns else "NULL AS liters_per_unit"
        group_by_optional = [
            clause
            for clause, column_name in (
                ("products.product_code", "product_code"),
                ("products.final_price_vat", "final_price_vat"),
                ("products.weight_kg", "weight_kg"),
                ("products.unit_price_per_kg", "unit_price_per_kg"),
                ("products.category", "category"),
                ("products.units_per_pack", "units_per_pack"),
                ("products.liters_per_unit", "liters_per_unit"),
            )
            if column_name in product_columns
        ]
        group_by_sql = ",\n                ".join(
            [
                "products.id",
                "products.product_name",
                "products.lot_code",
                "products.supplier_name",
                *group_by_optional,
            ]
        )
        rows = connection.execute(
            f"""
            SELECT
                products.id,
                products.product_name,
                products.lot_code,
                products.supplier_name,
                {select_product_code},
                {select_final_price_vat},
                {select_weight_kg},
                {select_unit_price_per_kg},
                {select_category},
                {select_units_per_pack},
                {select_liters_per_unit},
                COALESCE(SUM(items.quantity), 0) AS total_quantity,
                MAX(batches.confirmed_at) AS last_ordered_at
            FROM ordini_products AS products
            LEFT JOIN ordini_items AS items ON items.product_id = products.id
            LEFT JOIN ordini_batches AS batches ON batches.id = items.batch_id
            WHERE products.active = 1
            GROUP BY {group_by_sql}
            ORDER BY products.supplier_name ASC, products.product_name ASC, products.lot_code ASC
            """
        ).fetchall()

    return [
        ProductCandidate(
            id=int(row["id"]),
            product_name=row["product_name"],
            lot_code=row["lot_code"],
            supplier_name=row["supplier_name"],
            product_code=row["product_code"],
            final_price_vat=row["final_price_vat"],
            weight_kg=row["weight_kg"],
            unit_price_per_kg=row["unit_price_per_kg"],
            category=row["category"],
            units_per_pack=row["units_per_pack"],
            liters_per_unit=row["liters_per_unit"],
            total_quantity=int(row["total_quantity"] or 0),
            last_ordered_at=_serialize_datetime(row["last_ordered_at"]),
            score=0.0,
        )
        for row in rows
    ]


def _rank_products(products: list[ProductCandidate], query: str, limit: int | None) -> list[ProductCandidate]:
    if not query.strip():
        return products[:limit] if limit is not None else products

    requested_bucket = _extract_catalog_bucket(query)
    ranked: list[ProductCandidate] = []
    for product in products:
        searchable = " ".join(
            filter(
                None,
                [
                    product.product_name,
                    product.lot_code,
                    product.supplier_name,
                    product.product_code or "",
                    product.category or "",
                ],
            )
        )
        total_score = _score_product_match(query, searchable)
        if requested_bucket:
            bucket_labels = _product_bucket_labels_for_candidate(product)
            if requested_bucket in bucket_labels:
                total_score += 4.0 if total_score > 0 else 4.0
        if total_score <= 0:
            continue
        ranked.append(
            ProductCandidate(
                id=product.id,
                product_name=product.product_name,
                lot_code=product.lot_code,
                supplier_name=product.supplier_name,
                product_code=product.product_code,
                final_price_vat=product.final_price_vat,
                weight_kg=product.weight_kg,
                unit_price_per_kg=product.unit_price_per_kg,
                category=product.category,
                units_per_pack=product.units_per_pack,
                liters_per_unit=product.liters_per_unit,
                total_quantity=product.total_quantity,
                last_ordered_at=product.last_ordered_at,
                score=total_score,
            )
        )

    significant_tokens = _significant_catalog_query_tokens(query)
    if len(_catalog_query_tokens(query)) >= 2 and significant_tokens:
        exact_ranked = [
            product
            for product in ranked
            if _searchable_matches_all_query_tokens(
                " ".join(
                    filter(
                        None,
                        [
                            product.product_name,
                            product.lot_code,
                            product.supplier_name,
                            product.product_code or "",
                            product.category or "",
                        ],
                    )
                ),
                significant_tokens,
            )
        ]
        if exact_ranked:
            ranked = exact_ranked
        else:
            # A query like "cannucce laconi" must not degrade into "all Laconi products".
            # If no active product matches every meaningful token, returning nothing is safer
            # than presenting partial matches as grounded catalog data.
            ranked = []

    ranked.sort(
        key=lambda item: (
            -item.score,
            -item.total_quantity,
            item.supplier_name.lower(),
            item.product_name.lower(),
            item.lot_code.lower(),
        )
    )
    return ranked[:limit] if limit is not None else ranked


def _search_products(session: SessionIdentity, args: SearchProductsArgs) -> dict[str, object]:
    products = _load_product_candidates(session)
    ranked_all = _rank_products(products, args.query, None)
    ranked = ranked_all[: args.limit]
    family_request = sorted(_extract_requested_product_families(args.query))
    return {
        "query": args.query,
        "count": len(ranked_all),
        "family_request": family_request,
        "items": [
            {
                "id": product.id,
                "product_name": product.product_name,
                "likely_brand": _extract_likely_brand(product.product_name),
                "lot_code": product.lot_code,
                "supplier_name": product.supplier_name,
                "product_code": product.product_code,
                "final_price_vat": product.final_price_vat,
                "weight_kg": product.weight_kg,
                "unit_price_per_kg": product.unit_price_per_kg,
                "category": product.category,
                "units_per_pack": product.units_per_pack,
                "liters_per_unit": product.liters_per_unit,
                "total_quantity_ordered": product.total_quantity,
                "last_ordered_at": product.last_ordered_at,
                "match_score": round(product.score, 3),
            }
            for product in ranked
        ],
    }


def _year_bounds(year: int) -> tuple[str, str]:
    return (f"{year:04d}-01-01 00:00:00", f"{year + 1:04d}-01-01 00:00:00")


def _month_bounds(year: int, month: int) -> tuple[str, str]:
    start = datetime(year, month, 1)
    if month == 12:
        end = datetime(year + 1, 1, 1)
    else:
        end = datetime(year, month + 1, 1)
    return (start.strftime("%Y-%m-%d %H:%M:%S"), end.strftime("%Y-%m-%d %H:%M:%S"))


def _resolve_purchase_period_bounds(
    *,
    year: int | None,
    month: int | None,
    start_date: date | None,
    end_date: date | None,
) -> tuple[int | None, int | None, str | None, str | None]:
    effective_year = year
    effective_month = month

    if isinstance(start_date, date) and isinstance(end_date, date):
        if effective_year is None:
            effective_year = start_date.year
        inclusive_end = end_date - timedelta(days=1)
        if effective_month is None and start_date.year == inclusive_end.year and start_date.month == inclusive_end.month:
            effective_month = start_date.month
        return (
            effective_year,
            effective_month,
            datetime.combine(start_date, datetime.min.time()).strftime("%Y-%m-%d %H:%M:%S"),
            datetime.combine(end_date, datetime.min.time()).strftime("%Y-%m-%d %H:%M:%S"),
        )

    if month is not None and effective_year is None:
        effective_year = _today_in_timezone().year

    if effective_year is not None and month is not None:
        start_bound, end_bound = _month_bounds(effective_year, month)
        return effective_year, effective_month, start_bound, end_bound
    if effective_year is not None:
        start_bound, end_bound = _year_bounds(effective_year)
        return effective_year, effective_month, start_bound, end_bound
    return effective_year, effective_month, None, None


def _phone_digits(value: str | None) -> str:
    return "".join(char for char in (value or "") if char.isdigit())


def _normalize_customer_phone(value: str | None) -> str | None:
    cleaned = (value or "").strip()
    if not cleaned:
        return None
    if not re.fullmatch(r"[\d\s()+\-./]+", cleaned):
        return None

    has_plus_prefix = cleaned.startswith("+")
    digits = "".join(character for character in cleaned if character.isdigit())
    if len(digits) < 8 or len(digits) > 15:
        return None
    return f"+{digits}" if has_plus_prefix else digits


def _format_clock(value: time | str | None) -> str | None:
    if value is None:
        return None
    if isinstance(value, time):
        return value.strftime("%H:%M")
    text = str(value).strip()
    return text[:5] if len(text) >= 5 else text


def _clean_optional_text(value: str | None) -> str | None:
    if value is None:
        return None
    cleaned = value.strip()
    return cleaned or None


def _parse_small_number_token(value: str) -> int | None:
    cleaned = value.strip().casefold()
    if cleaned.isdigit():
        return int(cleaned)
    return _ITALIAN_SMALL_NUMBERS.get(cleaned)


def _extract_explicit_email(incoming_text: str) -> str | None:
    match = _EMAIL_PATTERN.search(incoming_text)
    if not match:
        return None
    return match.group(0).strip()


def _extract_explicit_phone(incoming_text: str) -> str | None:
    match = _PHONE_PATTERN.search(incoming_text)
    if not match:
        return None
    return _normalize_customer_phone(match.group(1))


def _extract_explicit_date(incoming_text: str) -> date | None:
    lowered = incoming_text.casefold()
    today_local = _today_in_timezone()
    if "dopodomani" in lowered:
        return today_local + timedelta(days=2)
    if "domani" in lowered:
        return today_local + timedelta(days=1)
    if "stasera" in lowered or "questa sera" in lowered or "oggi" in lowered:
        return today_local

    iso_match = re.search(r"\b(20\d{2})-(\d{2})-(\d{2})\b", incoming_text)
    if iso_match:
        try:
            return date.fromisoformat(f"{iso_match.group(1)}-{iso_match.group(2)}-{iso_match.group(3)}")
        except ValueError:
            return None

    slash_match = re.search(r"\b(\d{1,2})/(\d{1,2})(?:/(\d{2,4}))?\b", incoming_text)
    if not slash_match:
        return None

    day_value = int(slash_match.group(1))
    month_value = int(slash_match.group(2))
    year_value = slash_match.group(3)
    if year_value is None:
        year = today_local.year
    elif len(year_value) == 2:
        year = 2000 + int(year_value)
    else:
        year = int(year_value)

    try:
        resolved = date(year, month_value, day_value)
    except ValueError:
        return None

    if year_value is None and resolved < today_local:
        try:
            resolved = date(today_local.year + 1, month_value, day_value)
        except ValueError:
            return None
    return resolved


def _extract_explicit_times(incoming_text: str) -> list[time]:
    matches: list[time] = []
    seen: set[str] = set()

    for match in re.finditer(r"\b([01]?\d|2[0-3])[:.]([0-5]\d)\b", incoming_text):
        hour_value = int(match.group(1))
        minute_value = int(match.group(2))
        normalized = f"{hour_value:02d}:{minute_value:02d}"
        if normalized in seen:
            continue
        seen.add(normalized)
        matches.append(time(hour_value, minute_value))

    lowered = incoming_text.casefold()
    for match in re.finditer(r"\balle\s+([01]?\d|2[0-3])(?!\s*[:.]\d{2})\b", incoming_text, re.IGNORECASE):
        hour_value = int(match.group(1))
        if hour_value <= 12 and any(token in lowered for token in ("sera", "stasera", "pomeriggio")):
            hour_value = 12 if hour_value == 12 else hour_value + 12
        if hour_value <= 12:
            normalized = f"{hour_value:02d}:00"
        else:
            normalized = f"{hour_value:02d}:00"
        if normalized in seen:
            continue
        seen.add(normalized)
        matches.append(time.fromisoformat(normalized))

    return matches


def _extract_explicit_guest_count(incoming_text: str) -> int | None:
    patterns = [
        r"\bsiamo in (\d{1,3})\b",
        r"\bper (\d{1,3}) (?:persone|persona|coperti|coperto)\b",
        r"\b(\d{1,3}) (?:persone|persona|coperti|coperto)\b",
        r"\bsiamo in (un|una|uno|due|tre|quattro|cinque|sei|sette|otto|nove|dieci)\b",
        r"\bper (un|una|uno|due|tre|quattro|cinque|sei|sette|otto|nove|dieci) (?:persone|persona|coperti|coperto)\b",
        r"\b(un|una|uno|due|tre|quattro|cinque|sei|sette|otto|nove|dieci) (?:persone|persona|coperti|coperto)\b",
    ]
    for pattern in patterns:
        match = re.search(pattern, incoming_text, re.IGNORECASE)
        if not match:
            continue
        parsed = _parse_small_number_token(match.group(1))
        if parsed is not None:
            return parsed
    return None


def _is_meaningful_name(value: str | None) -> bool:
    if not value:
        return False
    if value.strip().casefold() in {"sconosciuto", "unknown"}:
        return False
    return any(character.isalpha() for character in value)


def _extract_explicit_name(incoming_text: str) -> str | None:
    patterns = [
        r"\ba\s+nome\s+di\s+([A-Za-zÀ-ÿ][A-Za-zÀ-ÿ' .-]{1,80}?)(?=\s+(?:per|il|domani|oggi|dopodomani|alle)\b|[,.]|$)",
        r"\ba\s+nome\s+([A-Za-zÀ-ÿ][A-Za-zÀ-ÿ' .-]{1,80}?)(?=\s+(?:per|il|domani|oggi|dopodomani|alle)\b|[,.]|$)",
        r"\bper\s+conto\s+di\s+([A-Za-zÀ-ÿ][A-Za-zÀ-ÿ' .-]{1,80}?)(?=\s+(?:per|il|domani|oggi|dopodomani|alle)\b|[,.]|$)",
        r"\bnome\s+([A-Za-zÀ-ÿ][A-Za-zÀ-ÿ' .-]{1,80}?)(?=\s+(?:per|il|domani|oggi|dopodomani|alle)\b|[,.]|$)",
    ]
    for pattern in patterns:
        match = re.search(pattern, incoming_text, re.IGNORECASE)
        if not match:
            continue
        candidate = re.sub(r"\s+", " ", match.group(1)).strip(" .,-")
        if _is_meaningful_name(candidate):
            return candidate
    return None


def _extract_reservation_customer_name(incoming_text: str) -> str | None:
    explicit = _extract_explicit_name(incoming_text)
    if explicit:
        return explicit

    cleaned = incoming_text
    cleaned = re.sub(r"(\+?\d[\d\s().\-\/]{5,}\d)", " ", cleaned)
    cleaned = _EMAIL_PATTERN.sub(" ", cleaned)
    cleaned = re.sub(r"\b([01]?\d|2[0-3])[:.]([0-5]\d)\b", " ", cleaned)
    cleaned = re.sub(r"\balle\s+([01]?\d|2[0-3])\b", " ", cleaned, flags=re.IGNORECASE)
    cleaned = re.sub(r"\b(20\d{2})-(\d{2})-(\d{2})\b", " ", cleaned)
    cleaned = re.sub(r"\b\d{1,2}/\d{1,2}(?:/\d{2,4})?\b", " ", cleaned)
    cleaned = re.sub(
        r"\b(?:aggiungi|aggiungere|crea|creare|inserisci|inserire|prenota|prenotare|prenotazione|prenotazioni|"
        r"modifica|modificare|sposta|spostare|cambia|cambiare|annulla|annullare|cancella|cancellare|elimina|eliminare|"
        r"domani|oggi|dopodomani|stasera|questa|sera|per|alle|di|il|la|una|un|sig|signor|signora)\b",
        " ",
        cleaned,
        flags=re.IGNORECASE,
    )
    cleaned = re.sub(
        r"\b(?:un|una|uno|due|tre|quattro|cinque|sei|sette|otto|nove|dieci|\d{1,3})\s+"
        r"(?:persone|persona|coperti|coperto)\b",
        " ",
        cleaned,
        flags=re.IGNORECASE,
    )
    cleaned = re.sub(r"[^\wÀ-ÿ' -]+", " ", cleaned)
    cleaned = re.sub(r"\s+", " ", cleaned).strip(" .,-")
    if not _is_meaningful_name(cleaned):
        return None
    return cleaned


def _extract_http_error_detail(exc: httpx.HTTPError) -> str:
    if isinstance(exc, httpx.HTTPStatusError):
        response = exc.response
        try:
            payload = response.json()
        except Exception:
            payload = None
        if isinstance(payload, dict):
            detail = payload.get("detail")
            if isinstance(detail, list):
                messages = []
                for item in detail:
                    if isinstance(item, dict):
                        item_message = str(item.get("msg") or item.get("message") or "").strip()
                        if item_message:
                            messages.append(item_message)
                if messages:
                    return " ".join(messages)
            if isinstance(detail, str) and detail.strip():
                return detail.strip()
            message = payload.get("message")
            if isinstance(message, str) and message.strip():
                return message.strip()
        body = response.text.strip()
        if body:
            return " ".join(body.split())[:300]
    return describe_llm_http_error(exc)


def _purchase_overview(session: SessionIdentity, args: PurchaseOverviewArgs) -> dict[str, object]:
    filters: list[str] = []
    params: list[object] = []
    effective_year, effective_month, start_bound, end_bound = _resolve_purchase_period_bounds(
        year=args.year,
        month=args.month,
        start_date=args.start_date,
        end_date=args.end_date,
    )
    if start_bound is not None and end_bound is not None:
        filters.append("batches.confirmed_at >= ? AND batches.confirmed_at < ?")
        params.extend([start_bound, end_bound])

    where_clause = f"WHERE {' AND '.join(filters)}" if filters else ""

    with _connect_orders_database(session) as connection:
        product_columns = _orders_table_columns(connection, "ordini_products")
        item_columns = _orders_table_columns(connection, "ordini_items")
        price_expr = "products.final_price_vat" if "final_price_vat" in product_columns else "NULL"
        snapshot_price_expr = "items.final_price_vat_snapshot" if "final_price_vat_snapshot" in item_columns else "NULL"
        line_total_expr = "items.estimated_line_total" if "estimated_line_total" in item_columns else "NULL"
        select_final_price_vat = f"{price_expr} AS final_price_vat"
        select_snapshot_price = f"{snapshot_price_expr} AS final_price_vat_snapshot"
        select_units_per_pack = "products.units_per_pack" if "units_per_pack" in product_columns else "NULL AS units_per_pack"
        select_liters_per_unit = "products.liters_per_unit" if "liters_per_unit" in product_columns else "NULL AS liters_per_unit"
        group_by_optional = [
            clause
            for clause, column_name in (
                ("products.final_price_vat", "final_price_vat"),
                ("items.final_price_vat_snapshot", "final_price_vat_snapshot"),
                ("products.units_per_pack", "units_per_pack"),
                ("products.liters_per_unit", "liters_per_unit"),
            )
            if column_name in product_columns
            or column_name in item_columns
        ]
        group_by_sql = ", ".join(
            [
                "items.product_id",
                "items.product_name",
                "items.lot_code",
                "items.supplier_name",
                *group_by_optional,
            ]
        )
        rows = connection.execute(
            f"""
            SELECT
                items.product_id,
                items.product_name,
                items.lot_code,
                items.supplier_name,
                {select_final_price_vat},
                {select_snapshot_price},
                {select_units_per_pack},
                {select_liters_per_unit},
                SUM(items.quantity) AS total_quantity,
                SUM(CASE WHEN {line_total_expr} IS NOT NULL THEN 1 ELSE 0 END) AS snapshot_priced_rows,
                SUM(CASE WHEN {line_total_expr} IS NULL AND {price_expr} IS NOT NULL THEN 1 ELSE 0 END) AS fallback_priced_rows,
                SUM(COALESCE({line_total_expr}, CASE WHEN {price_expr} IS NOT NULL THEN items.quantity * {price_expr} ELSE NULL END)) AS total_amount_sum,
                COUNT(DISTINCT items.batch_id) AS order_count,
                MAX(batches.confirmed_at) AS last_ordered_at
            FROM ordini_items AS items
            JOIN ordini_batches AS batches ON batches.id = items.batch_id
            LEFT JOIN ordini_products AS products ON products.id = items.product_id
            {where_clause}
            GROUP BY {group_by_sql}
            """,
            params,
        ).fetchall()

    ranked: list[tuple[float, sqlite3.Row]] = []
    for row in rows:
        searchable = f"{row['product_name']} {row['lot_code']} {row['supplier_name']}"
        score = _score_purchase_product_match(args.query, searchable) if args.query.strip() else 1.0
        if args.query.strip() and score <= 0:
            continue
        ranked.append((score, row))

    ranked.sort(
        key=lambda entry: (
            -entry[0],
            -int(entry[1]["total_quantity"] or 0),
            entry[1]["supplier_name"].lower(),
            entry[1]["product_name"].lower(),
        )
    )

    estimated_total_amount = 0.0
    priced_variant_count = 0
    missing_price_variant_count = 0
    missing_price_quantity = 0
    snapshot_priced_rows = 0
    fallback_priced_rows = 0
    for _, row in ranked:
        quantity = int(row["total_quantity"] or 0)
        estimated_amount = _coerce_positive_float(row["total_amount_sum"])
        if estimated_amount is None:
            missing_price_variant_count += 1
            missing_price_quantity += quantity
        else:
            priced_variant_count += 1
            estimated_total_amount += estimated_amount
        snapshot_priced_rows += int(row["snapshot_priced_rows"] or 0)
        fallback_priced_rows += int(row["fallback_priced_rows"] or 0)

    selected = ranked[: args.limit]
    supplier_counter = Counter(row["supplier_name"] for _, row in selected)
    brand_counter = Counter()
    for _, row in selected:
        brand = _extract_likely_brand(row["product_name"])
        if brand:
            brand_counter[brand] += int(row["total_quantity"] or 0)

    return {
        "query": args.query,
        "year": effective_year,
        "month": effective_month,
        "start_date": args.start_date.isoformat() if isinstance(args.start_date, date) else None,
        "end_date": args.end_date.isoformat() if isinstance(args.end_date, date) else None,
        "matched_count": len(ranked),
        "count": len(selected),
        "estimated_total_amount": round(estimated_total_amount, 2) if priced_variant_count else None,
        "priced_variant_count": priced_variant_count,
        "missing_price_variant_count": missing_price_variant_count,
        "missing_price_quantity": missing_price_quantity,
        "snapshot_priced_rows": snapshot_priced_rows,
        "fallback_priced_rows": fallback_priced_rows,
        "pricing_basis": "order_snapshot" if priced_variant_count and fallback_priced_rows == 0 else "snapshot_or_current_catalog",
        "top_suppliers": supplier_counter.most_common(),
        "likely_brands": [{"brand": brand, "total_quantity": quantity} for brand, quantity in brand_counter.most_common()],
        "items": [
            {
                "product_id": row["product_id"],
                "product_name": row["product_name"],
                "likely_brand": _extract_likely_brand(row["product_name"]),
                "lot_code": row["lot_code"],
                "supplier_name": row["supplier_name"],
                "final_price_vat": row["final_price_vat_snapshot"] if row["final_price_vat_snapshot"] is not None else row["final_price_vat"],
                "final_price_vat_snapshot": row["final_price_vat_snapshot"],
                "total_quantity": int(row["total_quantity"] or 0),
                "estimated_total_amount": _coerce_positive_float(row["total_amount_sum"]),
                "order_count": int(row["order_count"] or 0),
                "units_per_pack": row["units_per_pack"],
                "liters_per_unit": row["liters_per_unit"],
                "total_liters": _estimate_total_liters(
                    quantity=int(row["total_quantity"] or 0),
                    product_name=str(row["product_name"] or ""),
                    lot_code=str(row["lot_code"] or ""),
                    units_per_pack=row["units_per_pack"],
                    liters_per_unit=row["liters_per_unit"],
                ),
                "last_ordered_at": _serialize_datetime(row["last_ordered_at"]),
                "match_score": round(score, 3),
            }
            for score, row in selected
        ],
    }


def _purchase_period_comparison(session: SessionIdentity, args: PurchaseComparisonArgs) -> dict[str, object]:
    def load_period(year: int, month: int | None) -> dict[str, object]:
        start, end = _month_bounds(year, month) if month is not None else _year_bounds(year)
        with _connect_orders_database(session) as connection:
            product_columns = _orders_table_columns(connection, "ordini_products")
            item_columns = _orders_table_columns(connection, "ordini_items")
            price_expr = "products.final_price_vat" if "final_price_vat" in product_columns else "NULL"
            snapshot_price_expr = "items.final_price_vat_snapshot" if "final_price_vat_snapshot" in item_columns else "NULL"
            line_total_expr = "items.estimated_line_total" if "estimated_line_total" in item_columns else "NULL"
            select_final_price_vat = f"{price_expr} AS final_price_vat"
            select_snapshot_price = f"{snapshot_price_expr} AS final_price_vat_snapshot"
            select_line_total = f"{line_total_expr} AS estimated_line_total"
            rows = connection.execute(
                f"""
                SELECT
                    batches.id AS batch_id,
                    batches.confirmed_at,
                    items.product_name,
                    items.lot_code,
                    items.supplier_name,
                    items.quantity,
                    {select_final_price_vat},
                    {select_snapshot_price},
                    {select_line_total}
                FROM ordini_items AS items
                JOIN ordini_batches AS batches ON batches.id = items.batch_id
                LEFT JOIN ordini_products AS products ON products.id = items.product_id
                WHERE batches.confirmed_at >= ? AND batches.confirmed_at < ?
                ORDER BY batches.confirmed_at DESC, items.product_name ASC
                """,
                (start, end),
            ).fetchall()

        matched_rows: list[sqlite3.Row] = []
        distinct_batch_ids: set[int] = set()
        variants: dict[tuple[str, str, str], dict[str, object]] = {}
        for row in rows:
            searchable = f"{row['product_name']} {row['lot_code']} {row['supplier_name']}"
            score = _score_purchase_product_match(args.query, searchable) if args.query.strip() else 1.0
            if args.query.strip() and score <= 0:
                continue
            matched_rows.append(row)
            distinct_batch_ids.add(int(row["batch_id"]))
            key = (str(row["product_name"]), str(row["lot_code"]), str(row["supplier_name"]))
            entry = variants.setdefault(
                key,
                {
                    "product_name": row["product_name"],
                    "lot_code": row["lot_code"],
                    "supplier_name": row["supplier_name"],
                    "distinct_batch_ids": set(),
                    "total_quantity": 0,
                    "estimated_total_amount": 0.0,
                    "has_price_data": False,
                    "uses_fallback_price": False,
                    "last_ordered_at": _serialize_datetime(row["confirmed_at"]),
                },
            )
            batch_ids = entry["distinct_batch_ids"]
            if isinstance(batch_ids, set):
                batch_ids.add(int(row["batch_id"]))
            quantity = int(row["quantity"] or 0)
            entry["total_quantity"] = int(entry["total_quantity"]) + quantity
            estimated_amount = _coerce_positive_float(row["estimated_line_total"])
            if estimated_amount is None:
                estimated_amount = _estimate_total_amount(quantity=quantity, final_price_vat=row["final_price_vat"])
                if estimated_amount is not None:
                    entry["uses_fallback_price"] = True
            if estimated_amount is not None:
                entry["has_price_data"] = True
                entry["estimated_total_amount"] = float(entry["estimated_total_amount"]) + estimated_amount
            current_value = _serialize_datetime(row["confirmed_at"]) or ""
            if current_value and current_value > str(entry["last_ordered_at"] or ""):
                entry["last_ordered_at"] = current_value

        serialized_variants = []
        for entry in variants.values():
            batch_ids = entry["distinct_batch_ids"] if isinstance(entry["distinct_batch_ids"], set) else set()
            serialized_variants.append(
                {
                    "product_name": entry["product_name"],
                    "likely_brand": _extract_likely_brand(str(entry["product_name"])),
                    "lot_code": entry["lot_code"],
                    "supplier_name": entry["supplier_name"],
                    "order_count": len(batch_ids),
                    "total_quantity": int(entry["total_quantity"]),
                    "estimated_total_amount": round(float(entry["estimated_total_amount"]), 2) if entry["has_price_data"] else None,
                    "last_ordered_at": entry["last_ordered_at"],
                }
            )

        serialized_variants.sort(
            key=lambda item: (
                -int(item["order_count"]),
                -int(item["total_quantity"]),
                str(item["product_name"]).lower(),
            )
        )
        total_quantity = sum(int(item["total_quantity"]) for item in serialized_variants)
        priced_variants = [item for item in serialized_variants if item.get("estimated_total_amount") is not None]
        return {
            "year": year,
            "month": month,
            "query": args.query,
            "distinct_orders": len(distinct_batch_ids),
            "distinct_products": len(serialized_variants),
            "matched_rows": len(matched_rows),
            "total_quantity": total_quantity,
            "estimated_total_amount": round(sum(float(item.get("estimated_total_amount") or 0.0) for item in priced_variants), 2) if priced_variants else None,
            "missing_price_variant_count": sum(1 for item in serialized_variants if item.get("estimated_total_amount") is None),
            "fallback_price_variant_count": sum(1 for item in variants.values() if item.get("uses_fallback_price")),
            "pricing_basis": "order_snapshot" if priced_variants and not any(item.get("uses_fallback_price") for item in variants.values()) else "snapshot_or_current_catalog",
            "variants": serialized_variants[: args.limit],
        }

    primary = load_period(args.primary_year, args.primary_month)
    secondary = load_period(args.secondary_year, args.secondary_month)
    return {
        "query": args.query,
        "primary_year": args.primary_year,
        "primary_month": args.primary_month,
        "secondary_year": args.secondary_year,
        "secondary_month": args.secondary_month,
        "focus_hint": args.focus_hint,
        "percentage_requested": args.percentage_requested,
        "primary": primary,
        "secondary": secondary,
        "delta_products": int(primary["distinct_products"]) - int(secondary["distinct_products"]),
        "delta_orders": int(primary["distinct_orders"]) - int(secondary["distinct_orders"]),
        "delta_quantity": int(primary["total_quantity"]) - int(secondary["total_quantity"]),
    }


def _purchase_frequency(session: SessionIdentity, args: PurchaseFrequencyArgs) -> dict[str, object]:
    filters: list[str] = []
    params: list[object] = []
    effective_year, effective_month, start_bound, end_bound = _resolve_purchase_period_bounds(
        year=args.year,
        month=args.month,
        start_date=args.start_date,
        end_date=args.end_date,
    )
    if start_bound is not None and end_bound is not None:
        filters.append("batches.confirmed_at >= ? AND batches.confirmed_at < ?")
        params.extend([start_bound, end_bound])

    where_clause = f"WHERE {' AND '.join(filters)}" if filters else ""

    with _connect_orders_database(session) as connection:
        product_columns = _orders_table_columns(connection, "ordini_products")
        select_units_per_pack = "products.units_per_pack" if "units_per_pack" in product_columns else "NULL AS units_per_pack"
        select_liters_per_unit = "products.liters_per_unit" if "liters_per_unit" in product_columns else "NULL AS liters_per_unit"
        rows = connection.execute(
            f"""
            SELECT
                batches.id AS batch_id,
                batches.confirmed_at,
                items.product_name,
                items.lot_code,
                items.supplier_name,
                items.quantity,
                {select_units_per_pack},
                {select_liters_per_unit}
            FROM ordini_items AS items
            JOIN ordini_batches AS batches ON batches.id = items.batch_id
            LEFT JOIN ordini_products AS products ON products.id = items.product_id
            {where_clause}
            ORDER BY batches.confirmed_at DESC, items.product_name ASC
            """,
            params,
        ).fetchall()

    matched_rows: list[sqlite3.Row] = []
    distinct_batch_ids: set[int] = set()
    variants: dict[tuple[str, str, str], dict[str, object]] = {}
    for row in rows:
        searchable = f"{row['product_name']} {row['lot_code']} {row['supplier_name']}"
        score = _score_purchase_product_match(args.query, searchable) if args.query.strip() else 1.0
        if args.query.strip() and score <= 0:
            continue
        matched_rows.append(row)
        distinct_batch_ids.add(int(row["batch_id"]))
        key = (str(row["product_name"]), str(row["lot_code"]), str(row["supplier_name"]))
        entry = variants.setdefault(
            key,
            {
                "product_name": row["product_name"],
                "lot_code": row["lot_code"],
                "supplier_name": row["supplier_name"],
                "distinct_batch_ids": set(),
                "total_quantity": 0,
                "total_liters": 0.0,
                "first_ordered_at": _serialize_datetime(row["confirmed_at"]),
                "last_ordered_at": _serialize_datetime(row["confirmed_at"]),
                "units_per_pack": row["units_per_pack"],
                "liters_per_unit": row["liters_per_unit"],
            },
        )
        batch_ids = entry["distinct_batch_ids"]
        if isinstance(batch_ids, set):
            batch_ids.add(int(row["batch_id"]))
        entry["total_quantity"] = int(entry["total_quantity"]) + int(row["quantity"] or 0)
        line_liters = _estimate_total_liters(
            quantity=int(row["quantity"] or 0),
            product_name=str(row["product_name"] or ""),
            lot_code=str(row["lot_code"] or ""),
            units_per_pack=row["units_per_pack"],
            liters_per_unit=row["liters_per_unit"],
        )
        if line_liters is not None:
            entry["total_liters"] = float(entry["total_liters"]) + line_liters
        first_value = str(entry["first_ordered_at"] or "")
        last_value = str(entry["last_ordered_at"] or "")
        current_value = _serialize_datetime(row["confirmed_at"]) or ""
        if not first_value or current_value < first_value:
            entry["first_ordered_at"] = current_value
        if not last_value or current_value > last_value:
            entry["last_ordered_at"] = current_value

    serialized_variants = []
    for entry in variants.values():
        batch_ids = entry["distinct_batch_ids"] if isinstance(entry["distinct_batch_ids"], set) else set()
        serialized_variants.append(
            {
                "product_name": entry["product_name"],
                "likely_brand": _extract_likely_brand(str(entry["product_name"])),
                "lot_code": entry["lot_code"],
                "supplier_name": entry["supplier_name"],
                "order_count": len(batch_ids),
                "total_quantity": int(entry["total_quantity"]),
                "total_liters": round(float(entry["total_liters"]), 3) if float(entry["total_liters"]) > 0 else None,
                "units_per_pack": entry["units_per_pack"],
                "liters_per_unit": entry["liters_per_unit"],
                "first_ordered_at": entry["first_ordered_at"],
                "last_ordered_at": entry["last_ordered_at"],
            }
        )

    serialized_variants.sort(
        key=lambda item: (
            -int(item["order_count"]),
            -int(item["total_quantity"]),
            str(item["product_name"]).lower(),
        )
    )

    total_quantity = sum(int(item["total_quantity"]) for item in serialized_variants)
    total_liters = sum(float(item["total_liters"] or 0.0) for item in serialized_variants if item.get("total_liters") is not None)
    first_ordered_at = min((str(item["first_ordered_at"]) for item in serialized_variants if item.get("first_ordered_at")), default=None)
    last_ordered_at = max((str(item["last_ordered_at"]) for item in serialized_variants if item.get("last_ordered_at")), default=None)

    return {
        "query": args.query,
        "year": effective_year,
        "month": effective_month,
        "start_date": args.start_date.isoformat() if isinstance(args.start_date, date) else None,
        "end_date": args.end_date.isoformat() if isinstance(args.end_date, date) else None,
        "distinct_orders": len(distinct_batch_ids),
        "matched_rows": len(matched_rows),
        "total_quantity": total_quantity,
        "total_liters": round(total_liters, 3) if total_liters > 0 else None,
        "first_ordered_at": first_ordered_at,
        "last_ordered_at": last_ordered_at,
        "variants": serialized_variants[: args.limit],
    }


def _purchase_batches(session: SessionIdentity, args: PurchaseBatchesArgs) -> dict[str, object]:
    filters: list[str] = []
    params: list[object] = []
    effective_year = args.year
    effective_month = args.month
    if args.batch_id is not None:
        filters.append("batches.id = ?")
        params.append(args.batch_id)
    if args.target_date is not None and args.batch_id is None:
        start = datetime.combine(args.target_date, datetime.min.time()).strftime("%Y-%m-%d %H:%M:%S")
        end = (datetime.combine(args.target_date, datetime.min.time()) + timedelta(days=1)).strftime("%Y-%m-%d %H:%M:%S")
        filters.append("batches.confirmed_at >= ? AND batches.confirmed_at < ?")
        params.extend([start, end])
    elif args.batch_id is None:
        effective_year, effective_month, start_bound, end_bound = _resolve_purchase_period_bounds(
            year=args.year,
            month=args.month,
            start_date=args.start_date,
            end_date=args.end_date,
        )
        if start_bound is not None and end_bound is not None:
            filters.append("batches.confirmed_at >= ? AND batches.confirmed_at < ?")
            params.extend([start_bound, end_bound])

    where_clause = f"WHERE {' AND '.join(filters)}" if filters else ""

    with _connect_orders_database(session) as connection:
        batch_columns = _orders_table_columns(connection, "ordini_batches")
        item_columns = _orders_table_columns(connection, "ordini_items")
        product_columns = _orders_table_columns(connection, "ordini_products")
        batch_total_expr = "batches.total_estimated_amount" if "total_estimated_amount" in batch_columns else "NULL"
        snapshot_price_expr = "items.final_price_vat_snapshot" if "final_price_vat_snapshot" in item_columns else "NULL"
        line_total_expr = "items.estimated_line_total" if "estimated_line_total" in item_columns else "NULL"
        current_price_expr = "products.final_price_vat" if "final_price_vat" in product_columns else "NULL"
        rows = connection.execute(
            f"""
            SELECT
                batches.id AS batch_id,
                batches.staff,
                batches.confirmed_at,
                {batch_total_expr} AS total_estimated_amount,
                items.product_id,
                items.product_name,
                items.lot_code,
                items.supplier_name,
                items.quantity,
                {snapshot_price_expr} AS final_price_vat_snapshot,
                {line_total_expr} AS estimated_line_total,
                {current_price_expr} AS final_price_vat
            FROM ordini_batches AS batches
            JOIN ordini_items AS items ON items.batch_id = batches.id
            LEFT JOIN ordini_products AS products ON products.id = items.product_id
            {where_clause}
            ORDER BY batches.confirmed_at {'ASC' if args.sort_order == 'earliest' else 'DESC'}, items.id ASC
            """,
            params,
        ).fetchall()

    batches: dict[int, dict[str, object]] = {}
    for row in rows:
        batch_id = int(row["batch_id"])
        searchable = f"{row['product_name']} {row['lot_code']} {row['supplier_name']}"
        score = _score_purchase_product_match(args.query, searchable) if args.query.strip() else 1.0

        batch = batches.setdefault(
            batch_id,
            {
                "batch_id": batch_id,
                "staff": row["staff"],
                "confirmed_at": _serialize_datetime(row["confirmed_at"]),
                "total_estimated_amount": _coerce_positive_float(row["total_estimated_amount"]),
                "total_lines": 0,
                "total_quantity": 0,
                "supplier_names": set(),
                "items": [],
                "matched_items": [],
                "matched_total_quantity": 0,
                "priced_lines": 0,
                "missing_price_lines": 0,
                "uses_fallback_price": False,
                "match_score": 0.0 if args.query.strip() else 1.0,
            },
        )
        batch["total_lines"] = int(batch["total_lines"]) + 1
        quantity = int(row["quantity"] or 0)
        batch["total_quantity"] = int(batch["total_quantity"]) + quantity
        item_estimated_line_total = _coerce_positive_float(row["estimated_line_total"])
        if item_estimated_line_total is None:
            item_estimated_line_total = _estimate_total_amount(quantity=quantity, final_price_vat=row["final_price_vat"])
            if item_estimated_line_total is not None:
                batch["uses_fallback_price"] = True
        if item_estimated_line_total is not None:
            batch["priced_lines"] = int(batch["priced_lines"]) + 1
            if batch.get("total_estimated_amount") is None:
                running_total = _coerce_positive_float(batch.get("_calculated_total_amount")) or 0.0
                batch["_calculated_total_amount"] = round(running_total + item_estimated_line_total, 2)
        else:
            batch["missing_price_lines"] = int(batch["missing_price_lines"]) + 1
        cast_suppliers = batch["supplier_names"]
        if isinstance(cast_suppliers, set):
            cast_suppliers.add(str(row["supplier_name"]))
        item_payload = {
            "product_id": row["product_id"],
            "product_name": row["product_name"],
            "likely_brand": _extract_likely_brand(row["product_name"]),
            "lot_code": row["lot_code"],
            "supplier_name": row["supplier_name"],
            "quantity": quantity,
            "final_price_vat_snapshot": _coerce_positive_float(row["final_price_vat_snapshot"]),
            "final_price_vat": _coerce_positive_float(row["final_price_vat"]),
            "estimated_line_total": item_estimated_line_total,
        }
        batch["items"].append(item_payload)
        if args.query.strip() and score > 0:
            matched_items = batch.get("matched_items")
            if isinstance(matched_items, list):
                matched_items.append(item_payload)
            batch["matched_total_quantity"] = int(batch["matched_total_quantity"]) + quantity
        if args.query.strip() and score > float(batch["match_score"]):
            batch["match_score"] = score

    selected_batches = []
    for batch in batches.values():
        score = float(batch["match_score"])
        if args.query.strip() and score <= 0:
            continue
        supplier_names = sorted(str(name) for name in batch["supplier_names"]) if isinstance(batch["supplier_names"], set) else []
        items = batch["items"] if isinstance(batch["items"], list) else []
        total_estimated_amount = _coerce_positive_float(batch.get("total_estimated_amount"))
        if total_estimated_amount is None:
            total_estimated_amount = _coerce_positive_float(batch.get("_calculated_total_amount"))
        selected_batches.append(
            {
                "batch_id": batch["batch_id"],
                "staff": batch["staff"],
                "confirmed_at": batch["confirmed_at"],
                "total_estimated_amount": total_estimated_amount,
                "total_lines": batch["total_lines"],
                "total_quantity": batch["total_quantity"],
                "supplier_names": supplier_names,
                "priced_lines": batch["priced_lines"],
                "missing_price_lines": batch["missing_price_lines"],
                "pricing_basis": "order_snapshot"
                if total_estimated_amount is not None and not bool(batch.get("uses_fallback_price"))
                else "snapshot_or_current_catalog",
                "items": items,
                "matched_items": batch.get("matched_items") if isinstance(batch.get("matched_items"), list) else [],
                "matched_total_quantity": int(batch.get("matched_total_quantity") or 0),
                "match_score": round(score, 3),
            }
        )

    if args.query.strip():
        selected_batches.sort(key=lambda batch: str(batch["confirmed_at"] or ""), reverse=args.sort_order != "earliest")
        selected_batches.sort(key=lambda batch: float(batch["match_score"]), reverse=True)
    else:
        selected_batches.sort(key=lambda batch: str(batch["confirmed_at"] or ""), reverse=args.sort_order != "earliest")
    selected_batches = selected_batches[: args.limit]

    return {
        "query": args.query,
        "batch_id": args.batch_id,
        "target_date": args.target_date.isoformat() if isinstance(args.target_date, date) else None,
        "year": effective_year,
        "month": effective_month,
        "start_date": args.start_date.isoformat() if isinstance(args.start_date, date) else None,
        "end_date": args.end_date.isoformat() if isinstance(args.end_date, date) else None,
        "sort_order": args.sort_order,
        "count": len(selected_batches),
        "batches": selected_batches,
    }


def _purchase_history(session: SessionIdentity, args: PurchaseHistoryArgs) -> dict[str, object]:
    filters: list[str] = []
    params: list[object] = []
    effective_year, effective_month, start_bound, end_bound = _resolve_purchase_period_bounds(
        year=args.year,
        month=args.month,
        start_date=args.start_date,
        end_date=args.end_date,
    )
    if start_bound is not None and end_bound is not None:
        filters.append("batches.confirmed_at >= ? AND batches.confirmed_at < ?")
        params.extend([start_bound, end_bound])

    where_clause = f"WHERE {' AND '.join(filters)}" if filters else ""

    with _connect_orders_database(session) as connection:
        rows = connection.execute(
            f"""
            SELECT
                batches.id AS batch_id,
                batches.staff,
                batches.confirmed_at,
                items.product_id,
                items.product_name,
                items.lot_code,
                items.supplier_name,
                items.quantity
            FROM ordini_items AS items
            JOIN ordini_batches AS batches ON batches.id = items.batch_id
            {where_clause}
            ORDER BY batches.confirmed_at DESC, items.product_name ASC
            """,
            params,
        ).fetchall()

    filtered: list[tuple[float, sqlite3.Row]] = []
    for row in rows:
        searchable = f"{row['product_name']} {row['lot_code']} {row['supplier_name']}"
        score = _score_purchase_product_match(args.query, searchable) if args.query.strip() else 1.0
        if args.query.strip() and score <= 0:
            continue
        filtered.append((score, row))

    filtered.sort(key=lambda entry: str(entry[1]["confirmed_at"]), reverse=True)
    filtered.sort(key=lambda entry: entry[0], reverse=True)
    selected = filtered[: args.limit]
    return {
        "query": args.query,
        "year": effective_year,
        "month": effective_month,
        "start_date": args.start_date.isoformat() if isinstance(args.start_date, date) else None,
        "end_date": args.end_date.isoformat() if isinstance(args.end_date, date) else None,
        "count": len(selected),
        "total_matches": len(filtered),
        "items": [
            {
                "batch_id": row["batch_id"],
                "staff": row["staff"],
                "confirmed_at": _serialize_datetime(row["confirmed_at"]),
                "product_id": row["product_id"],
                "product_name": row["product_name"],
                "likely_brand": _extract_likely_brand(row["product_name"]),
                "lot_code": row["lot_code"],
                "supplier_name": row["supplier_name"],
                "quantity": int(row["quantity"] or 0),
                "match_score": round(score, 3),
            }
            for score, row in selected
        ],
    }


def _normalize_suspended_payload(raw_payload: object) -> dict[str, int]:
    if isinstance(raw_payload, str):
        try:
            raw_payload = json.loads(raw_payload)
        except json.JSONDecodeError:
            return {}

    if not isinstance(raw_payload, dict):
        return {}

    normalized: dict[str, int] = {}
    for key, value in raw_payload.items():
        try:
            quantity = int(value)
        except (TypeError, ValueError):
            continue
        if quantity > 0:
            normalized[str(key)] = quantity
    return normalized


def _read_suspended_order(session: SessionIdentity, args: SuspendedOrderReadArgs) -> dict[str, object]:
    staff = (args.staff or _default_staff(session)).strip().lower()
    with _connect_orders_database(session) as connection:
        row = connection.execute("SELECT staff, payload, updated_at FROM ordini_suspended_orders WHERE staff = ?", (staff,)).fetchone()
        if row is None:
            return {"staff": staff, "exists": False, "items": []}

        payload = _normalize_suspended_payload(row["payload"])
        product_rows = connection.execute(
            f"SELECT id, product_name, lot_code, supplier_name FROM ordini_products WHERE id IN ({','.join(['?'] * len(payload))})",
            tuple(int(product_id) for product_id in payload),
        ).fetchall() if payload else []
        products_by_id = {str(product["id"]): product for product in product_rows}

    items = []
    for product_id, quantity in payload.items():
        product = products_by_id.get(product_id)
        items.append(
            {
                "product_id": int(product_id),
                "quantity": quantity,
                "product_name": product["product_name"] if product is not None else None,
                "lot_code": product["lot_code"] if product is not None else None,
                "supplier_name": product["supplier_name"] if product is not None else None,
            }
        )

    items.sort(key=lambda item: ((item["product_name"] or "").lower(), int(item["product_id"])))
    return {
        "staff": row["staff"],
        "exists": True,
        "updated_at": _serialize_datetime(row["updated_at"]),
        "items": items,
    }


def _resolve_single_product(products: list[ProductCandidate], query: str) -> tuple[str, ProductCandidate | None, list[ProductCandidate]]:
    ranked = _rank_products(products, query, 5)
    if not ranked:
        return "not_found", None, []

    if len(ranked) == 1:
        return "matched", ranked[0], ranked

    top = ranked[0]
    second = ranked[1]
    if top.score >= second.score + 1.5:
        return "matched", top, ranked

    normalized_query = _normalize_text(query)
    if normalized_query and normalized_query == _normalize_text(top.product_name):
        return "matched", top, ranked

    return "ambiguous", None, ranked


def _is_pending_product_identity_value(value: str | None, *, placeholder: str) -> bool:
    cleaned = _clean_optional_text(value)
    if not cleaned:
        return True
    return _normalize_text(cleaned) == _normalize_text(placeholder)


def _normalize_product_identity_value(value: str | None, *, placeholder: str) -> str:
    cleaned = _clean_optional_text(value)
    return cleaned or placeholder


def _incomplete_product_identity_fields(*, lot_code: str | None, supplier_name: str | None) -> list[str]:
    missing_fields: list[str] = []
    if _is_pending_product_identity_value(lot_code, placeholder=_PENDING_PRODUCT_LOT_CODE):
        missing_fields.append("lotto")
    if _is_pending_product_identity_value(supplier_name, placeholder=_PENDING_PRODUCT_SUPPLIER_NAME):
        missing_fields.append("fornitore")
    return missing_fields


def _find_existing_product_for_upsert(
    connection: sqlite3.Connection,
    *,
    product_name: str,
    lot_code: str | None,
    supplier_name: str | None,
) -> sqlite3.Row | None:
    target_lot_code = _normalize_product_identity_value(lot_code, placeholder=_PENDING_PRODUCT_LOT_CODE)
    target_supplier_name = _normalize_product_identity_value(supplier_name, placeholder=_PENDING_PRODUCT_SUPPLIER_NAME)

    exact = connection.execute(
        """
        SELECT *
        FROM ordini_products
        WHERE product_name = ? AND lot_code = ? AND supplier_name = ?
        ORDER BY id DESC
        LIMIT 1
        """,
        (product_name, target_lot_code, target_supplier_name),
    ).fetchone()
    if exact is not None:
        return exact

    candidates = connection.execute(
        """
        SELECT *
        FROM ordini_products
        WHERE product_name = ?
        ORDER BY updated_at DESC, id DESC
        """,
        (product_name,),
    ).fetchall()
    if not candidates:
        return None

    ranked_candidates = sorted(
        candidates,
        key=lambda row: (
            int(_is_pending_product_identity_value(row["lot_code"], placeholder=_PENDING_PRODUCT_LOT_CODE)),
            int(_is_pending_product_identity_value(row["supplier_name"], placeholder=_PENDING_PRODUCT_SUPPLIER_NAME)),
            int(row["id"]),
        ),
        reverse=True,
    )
    for candidate in ranked_candidates:
        candidate_lot_code = _normalize_product_identity_value(candidate["lot_code"], placeholder=_PENDING_PRODUCT_LOT_CODE)
        candidate_supplier_name = _normalize_product_identity_value(candidate["supplier_name"], placeholder=_PENDING_PRODUCT_SUPPLIER_NAME)
        if not (
            _is_pending_product_identity_value(candidate_lot_code, placeholder=_PENDING_PRODUCT_LOT_CODE)
            or _is_pending_product_identity_value(candidate_supplier_name, placeholder=_PENDING_PRODUCT_SUPPLIER_NAME)
        ):
            continue
        if lot_code and candidate_lot_code not in {lot_code, _PENDING_PRODUCT_LOT_CODE}:
            continue
        if supplier_name and candidate_supplier_name not in {supplier_name, _PENDING_PRODUCT_SUPPLIER_NAME}:
            continue
        return candidate

    return None


def _product_candidate_identity_quality(candidate: ProductCandidate) -> int:
    return int(not _is_pending_product_identity_value(candidate.lot_code, placeholder=_PENDING_PRODUCT_LOT_CODE)) + int(
        not _is_pending_product_identity_value(candidate.supplier_name, placeholder=_PENDING_PRODUCT_SUPPLIER_NAME)
    )


def _product_row_identity_quality(row: sqlite3.Row | None) -> int:
    if row is None:
        return 0
    return int(not _is_pending_product_identity_value(_row_get(row, "lot_code"), placeholder=_PENDING_PRODUCT_LOT_CODE)) + int(
        not _is_pending_product_identity_value(_row_get(row, "supplier_name"), placeholder=_PENDING_PRODUCT_SUPPLIER_NAME)
    )


def _resolve_existing_product_for_update(
    session: SessionIdentity,
    connection: sqlite3.Connection,
    *,
    product_name: str,
    lot_code: str | None,
    supplier_name: str | None,
) -> tuple[sqlite3.Row | None, bool]:
    if lot_code or supplier_name or not product_name.strip():
        return None, False

    ranked = _rank_products(_load_product_candidates(session), product_name, 8)
    if not ranked:
        return None, False

    ranked.sort(
        key=lambda candidate: (
            candidate.score,
            _product_candidate_identity_quality(candidate),
            candidate.total_quantity,
            int(bool(candidate.last_ordered_at)),
        ),
        reverse=True,
    )

    best = ranked[0]
    if best.score <= 0:
        return None, False

    best_identity = _product_candidate_identity_quality(best)
    query_tokens = _tokenize_query(product_name)
    if best_identity < 2 and len(query_tokens) <= 3:
        richer_candidates = [candidate for candidate in ranked if _product_candidate_identity_quality(candidate) > best_identity]
        if len(richer_candidates) == 1:
            best = richer_candidates[0]
        elif len(richer_candidates) > 1 and richer_candidates[0].score >= richer_candidates[1].score + 1.0:
            best = richer_candidates[0]

    second = ranked[1] if len(ranked) > 1 else None
    if second is not None:
        best_identity = _product_candidate_identity_quality(best)
        second_identity = _product_candidate_identity_quality(second)
        clearly_better = best.score >= second.score + 1.25
        clearly_more_specific = (
            best_identity > second_identity
            and second_identity < 2
            and (
                best.score >= second.score
                or (len(query_tokens) <= 3 and best_identity == 2)
            )
        )
        if not clearly_better and not clearly_more_specific:
            return None, True

    row = connection.execute("SELECT * FROM ordini_products WHERE id = ? LIMIT 1", (best.id,)).fetchone()
    return row, False


def _upsert_product(session: SessionIdentity, args: ProductWriteArgs) -> dict[str, object]:
    operation = str(args.operation or "upsert").strip()
    product_name = _clean_optional_text(args.product_name)
    lot_code = _clean_optional_text(args.lot_code)
    supplier_name = _clean_optional_text(args.supplier_name)
    if not product_name:
        return {
            "status": "clarification_required",
            "missing_fields": ["nome prodotto"],
            "detail": (
                "Per registrare il prodotto scrivimi almeno il nome prodotto. "
                "Se li hai, aggiungi anche lotto, fornitore, codice, prezzo, iva, categoria e note. "
                "Se qualche dato ti manca, lo salvo comunque e lo completiamo dopo."
            ),
        }

    with _connect_orders_database(session) as connection:
        if operation == "delete":
            candidate_rows = connection.execute(
                """
                SELECT *
                FROM ordini_products
                WHERE active = 1
                  AND product_name = ?
                  AND (? IS NULL OR lot_code = ?)
                  AND (? IS NULL OR supplier_name = ?)
                ORDER BY updated_at DESC, id DESC
                """,
                (product_name, lot_code, lot_code, supplier_name, supplier_name),
            ).fetchall()
            if not candidate_rows:
                return {"status": "not_found", "detail": "Non trovo un prodotto attivo corrispondente da eliminare."}
            if len(candidate_rows) > 1 and (not lot_code and not supplier_name):
                return {
                    "status": "clarification_required",
                    "detail": "Ho trovato piu varianti del prodotto. Indicami almeno lotto o fornitore per eliminarlo in modo affidabile.",
                }
            target_row = candidate_rows[0]
            connection.execute(
                "UPDATE ordini_products SET active = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?",
                (int(target_row["id"]),),
            )
            connection.commit()
            return {
                "status": "deleted",
                "product": {
                    "id": int(target_row["id"]),
                    "product_name": target_row["product_name"],
                    "lot_code": target_row["lot_code"],
                    "supplier_name": target_row["supplier_name"],
                    "product_code": _row_get(target_row, "product_code"),
                    "final_price_vat": _row_get(target_row, "final_price_vat"),
                    "unit_price_per_kg": _row_get(target_row, "unit_price_per_kg"),
                    "category": _row_get(target_row, "category"),
                },
            }

        existing = _find_existing_product_for_upsert(
            connection,
            product_name=product_name,
            lot_code=lot_code,
            supplier_name=supplier_name,
        )
        product_columns = _orders_table_columns(connection, "ordini_products")

        optional_fields = {
            "product_code": _clean_optional_text(args.product_code),
            "final_price_vat": args.final_price_vat,
            "vat_rate": args.vat_rate,
            "weight_kg": args.weight_kg,
            "unit_price_per_kg": args.unit_price_per_kg,
            "category": _clean_optional_text(args.category),
            "notes": _clean_optional_text(args.notes),
            "units_per_pack": args.units_per_pack,
            "liters_per_unit": args.liters_per_unit,
        }
        optional_fields = {field_name: value for field_name, value in optional_fields.items() if field_name in product_columns}
        update_fields_present = any(value is not None for value in optional_fields.values())
        normalized_lot_code = _normalize_product_identity_value(lot_code, placeholder=_PENDING_PRODUCT_LOT_CODE)
        normalized_supplier_name = _normalize_product_identity_value(supplier_name, placeholder=_PENDING_PRODUCT_SUPPLIER_NAME)

        if update_fields_present and not lot_code and not supplier_name:
            resolved_existing, ambiguous_resolution = _resolve_existing_product_for_update(
                session,
                connection,
                product_name=product_name,
                lot_code=lot_code,
                supplier_name=supplier_name,
            )
            if ambiguous_resolution and existing is None:
                return {
                    "status": "clarification_required",
                    "detail": (
                        "Ho trovato piu prodotti compatibili con l'aggiornamento. "
                        "Indicami il nome completo oppure aggiungi lotto o fornitore per aggiornare la variante giusta."
                    ),
                }
            if resolved_existing is not None:
                should_replace_existing = existing is None or (
                    _product_row_identity_quality(existing) < _product_row_identity_quality(resolved_existing)
                )
                if should_replace_existing:
                    existing = resolved_existing
                    product_name = _clean_optional_text(existing["product_name"]) or product_name

        if existing is None:
            columns = ["product_name", "lot_code", "supplier_name", "active"]
            values: list[object] = [product_name, normalized_lot_code, normalized_supplier_name, 1]
            for field_name, value in optional_fields.items():
                if value is None:
                    continue
                columns.append(field_name)
                values.append(value)
            placeholders = ", ".join("?" for _ in columns)
            connection.execute(
                f"INSERT INTO ordini_products ({', '.join(columns)}) VALUES ({placeholders})",
                tuple(values),
            )
            connection.commit()
            product_row = connection.execute(
                """
                SELECT *
                FROM ordini_products
                WHERE product_name = ? AND lot_code = ? AND supplier_name = ?
                ORDER BY id DESC
                LIMIT 1
                """,
                (product_name, normalized_lot_code, normalized_supplier_name),
            ).fetchone()
            if product_row is None:
                raise HTTPException(status_code=500, detail="Prodotto creato ma non ricaricabile")
            pending_fields = _incomplete_product_identity_fields(
                lot_code=product_row["lot_code"],
                supplier_name=product_row["supplier_name"],
            )
            return {
                "status": "created",
                "pending_fields": pending_fields,
                "product": {
                    "id": int(product_row["id"]),
                    "product_name": product_row["product_name"],
                    "lot_code": product_row["lot_code"],
                    "supplier_name": product_row["supplier_name"],
                    "product_code": _row_get(product_row, "product_code"),
                    "final_price_vat": _row_get(product_row, "final_price_vat"),
                    "unit_price_per_kg": _row_get(product_row, "unit_price_per_kg"),
                    "category": _row_get(product_row, "category"),
                },
            }

        assignments: list[str] = ["active = 1", "updated_at = CURRENT_TIMESTAMP"]
        params: list[object] = []
        next_lot_code = lot_code or _normalize_product_identity_value(existing["lot_code"], placeholder=_PENDING_PRODUCT_LOT_CODE)
        next_supplier_name = supplier_name or _normalize_product_identity_value(existing["supplier_name"], placeholder=_PENDING_PRODUCT_SUPPLIER_NAME)
        if _clean_optional_text(existing["product_name"]) != product_name:
            assignments.append("product_name = ?")
            params.append(product_name)
        if _clean_optional_text(existing["lot_code"]) != next_lot_code:
            assignments.append("lot_code = ?")
            params.append(next_lot_code)
        if _clean_optional_text(existing["supplier_name"]) != next_supplier_name:
            assignments.append("supplier_name = ?")
            params.append(next_supplier_name)
        for field_name, value in optional_fields.items():
            if value is None:
                continue
            assignments.append(f"{field_name} = ?")
            params.append(value)
        if len(assignments) > 2:
            connection.execute(
                f"UPDATE ordini_products SET {', '.join(assignments)} WHERE id = ?",
                tuple(params + [int(existing["id"])]),
            )
        else:
            connection.execute(
                "UPDATE ordini_products SET active = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?",
                (int(existing["id"]),),
            )
        connection.commit()
        product_row = connection.execute("SELECT * FROM ordini_products WHERE id = ?", (int(existing["id"]),)).fetchone()
        if product_row is None:
            raise HTTPException(status_code=500, detail="Prodotto aggiornato ma non ricaricabile")
        pending_fields = _incomplete_product_identity_fields(
            lot_code=product_row["lot_code"],
            supplier_name=product_row["supplier_name"],
        )
        return {
            "status": "updated",
            "pending_fields": pending_fields,
            "product": {
                "id": int(product_row["id"]),
                "product_name": product_row["product_name"],
                "lot_code": product_row["lot_code"],
                "supplier_name": product_row["supplier_name"],
                "product_code": _row_get(product_row, "product_code"),
                "final_price_vat": _row_get(product_row, "final_price_vat"),
                "unit_price_per_kg": _row_get(product_row, "unit_price_per_kg"),
                "category": _row_get(product_row, "category"),
            },
        }


def _write_suspended_order(session: SessionIdentity, args: SuspendedOrderWriteArgs) -> dict[str, object]:
    staff = (args.staff or _default_staff(session)).strip().lower()
    all_products = _load_product_candidates(session)

    resolved_items: list[dict[str, object]] = []
    missing_items: list[dict[str, object]] = []
    ambiguous_items: list[dict[str, object]] = []

    for item in args.items:
        status, matched_product, ranked = _resolve_single_product(all_products, item.product_query)
        if status == "matched" and matched_product is not None:
            resolved_items.append(
                {
                    "product_id": matched_product.id,
                    "product_name": matched_product.product_name,
                    "lot_code": matched_product.lot_code,
                    "supplier_name": matched_product.supplier_name,
                    "quantity": item.quantity,
                }
            )
            continue

        if status == "not_found":
            missing_items.append({"product_query": item.product_query, "quantity": item.quantity})
            continue

        ambiguous_items.append(
            {
                "product_query": item.product_query,
                "quantity": item.quantity,
                "options": [
                    {
                        "product_id": candidate.id,
                        "product_name": candidate.product_name,
                        "lot_code": candidate.lot_code,
                        "supplier_name": candidate.supplier_name,
                    }
                    for candidate in ranked
                ],
            }
        )

    if missing_items or ambiguous_items:
        return {
            "status": "clarification_required",
            "staff": staff,
            "resolved_items": resolved_items,
            "missing_items": missing_items,
            "ambiguous_items": ambiguous_items,
        }

    with _connect_orders_database(session) as connection:
        current_row = connection.execute(
            "SELECT id, payload, updated_at FROM ordini_suspended_orders WHERE staff = ?",
            (staff,),
        ).fetchone()
        current_payload = _normalize_suspended_payload(current_row["payload"]) if current_row is not None else {}
        next_payload = {} if args.operation == "set" else dict(current_payload)

        for item in resolved_items:
            product_id = str(item["product_id"])
            quantity = int(item["quantity"])
            if args.operation == "add":
                next_payload[product_id] = int(next_payload.get(product_id, 0)) + quantity
            else:
                next_payload[product_id] = quantity

        if current_row is None:
            connection.execute(
                "INSERT INTO ordini_suspended_orders (staff, payload) VALUES (?, ?)",
                (staff, json.dumps(next_payload, ensure_ascii=False)),
            )
        else:
            connection.execute(
                "UPDATE ordini_suspended_orders SET payload = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?",
                (json.dumps(next_payload, ensure_ascii=False), current_row["id"]),
            )
        connection.commit()

    result = _read_suspended_order(session, SuspendedOrderReadArgs(staff=staff))
    result["status"] = "updated"
    result["operation"] = args.operation
    result["added_or_set_items"] = resolved_items
    return result


def _goal_matches(item: sqlite3.Row, goal: sqlite3.Row, *, secondary: bool = False) -> bool:
    product_match = goal["secondary_product_match"] if secondary else goal["product_match"]
    if secondary and not product_match:
        return False
    product_terms = _split_goal_match_terms(product_match if isinstance(product_match, str) else None)
    if product_terms:
        product_name = str(item["product_name"] or "").lower()
        if not any(term.lower() in product_name for term in product_terms):
            return False
    supplier_match = goal["supplier_match"]
    if supplier_match and supplier_match.lower() not in item["supplier_name"].lower():
        return False
    return True


def _find_liters_goal_missing_pack_sizes(
    connection: sqlite3.Connection,
    *,
    supplier_match: str | None,
    product_match: str | None,
    secondary_product_match: str | None = None,
) -> list[dict[str, str]]:
    rows = connection.execute(
        """
        SELECT product_name, lot_code, supplier_name, units_per_pack
        FROM ordini_products
        WHERE active = 1
        ORDER BY supplier_name ASC, product_name ASC, lot_code ASC
        """
    ).fetchall()

    goal_like = {
        "product_match": product_match,
        "secondary_product_match": secondary_product_match,
        "supplier_match": supplier_match,
    }
    missing: list[dict[str, str]] = []
    seen: set[tuple[str, str, str]] = set()
    for row in rows:
        if not (_goal_matches(row, goal_like) or (secondary_product_match and _goal_matches(row, goal_like, secondary=True))):
            continue
        if not _lot_requires_units_per_pack(str(row["lot_code"] or "")):
            continue
        if _coerce_positive_float(row["units_per_pack"]) is not None:
            continue
        key = (str(row["product_name"]), str(row["lot_code"]), str(row["supplier_name"]))
        if key in seen:
            continue
        seen.add(key)
        missing.append(
            {
                "product_name": str(row["product_name"] or ""),
                "lot_code": str(row["lot_code"] or ""),
                "supplier_name": str(row["supplier_name"] or ""),
            }
        )
    return missing


def _sales_goals(session: SessionIdentity, args: SalesGoalsArgs) -> dict[str, object]:
    target_year = args.year or _today_in_timezone().year
    start, end = _year_bounds(target_year)
    with _connect_orders_database(session) as connection:
        product_columns = _orders_table_columns(connection, "ordini_products")
        select_product_units_per_pack = "products.units_per_pack" if "units_per_pack" in product_columns else "NULL"
        select_product_liters_per_unit = "products.liters_per_unit" if "liters_per_unit" in product_columns else "NULL"
        goals = connection.execute(
            """
            SELECT *
            FROM ordini_seasonal_goals
            WHERE year = ?
            ORDER BY CASE WHEN goal_type = 'note' THEN 1 ELSE 0 END ASC, id ASC
            """,
            (target_year,),
        ).fetchall()
        items = connection.execute(
            """
            SELECT
                items.*,
                batches.confirmed_at,
                COALESCE(items.units_per_pack, """
            + select_product_units_per_pack
            + """) AS effective_units_per_pack,
                COALESCE(items.liters_per_unit, """
            + select_product_liters_per_unit
            + """) AS effective_liters_per_unit
            FROM ordini_items AS items
            JOIN ordini_batches AS batches ON batches.id = items.batch_id
            LEFT JOIN ordini_products AS products ON products.id = items.product_id
            WHERE batches.confirmed_at >= ? AND batches.confirmed_at < ?
            """,
            (start, end),
        ).fetchall()

    serialized_goals = []
    for goal in goals:
        if goal["goal_type"] == "note":
            serialized_goals.append(
                {
                    "id": goal["id"],
                    "name": goal["name"],
                    "type": "note",
                    "description": goal["description"] or "",
                }
            )
            continue

        if goal["goal_type"] == "liters_dual":
            primary = 0.0
            secondary = 0.0
            for item in items:
                liters = _estimate_total_liters(
                    quantity=int(item["quantity"] or 0),
                    product_name=str(item["product_name"] or ""),
                    lot_code=str(item["lot_code"] or ""),
                    units_per_pack=item["effective_units_per_pack"],
                    liters_per_unit=item["effective_liters_per_unit"],
                )
                if liters is None or liters <= 0:
                    continue
                if _goal_matches(item, goal):
                    primary += liters
                if _goal_matches(item, goal, secondary=True):
                    secondary += liters
            serialized_goals.append(
                {
                    "id": goal["id"],
                    "name": goal["name"],
                    "type": "liters_dual",
                    "target_grey": goal["target"] or 0,
                    "target_patron": goal["secondary_target"] or 0,
                    "progress_grey": round(primary, 2),
                    "progress_patron": round(secondary, 2),
                    "bonus": goal["bonus_label"],
                    "description": goal["description"] or "",
                }
            )
            continue

        progress = 0.0
        for item in items:
            if not _goal_matches(item, goal):
                continue
            if goal["goal_type"] == "liters":
                liters = _estimate_total_liters(
                    quantity=int(item["quantity"] or 0),
                    product_name=str(item["product_name"] or ""),
                    lot_code=str(item["lot_code"] or ""),
                    units_per_pack=item["effective_units_per_pack"],
                    liters_per_unit=item["effective_liters_per_unit"],
                )
                if liters is None:
                    continue
                progress += liters
            else:
                progress += int(item["quantity"] or 0)
        serialized_goals.append(
            {
                "id": goal["id"],
                "name": goal["name"],
                "type": goal["goal_type"],
                "target": goal["target"] or 0,
                "progress": round(progress, 2),
                "unit": goal["unit_label"] or ("L" if goal["goal_type"] == "liters" else "articoli"),
                "bonus": goal["bonus_label"],
                "description": goal["description"] or "",
            }
        )

    return {"year": target_year, "count": len(serialized_goals), "goals": serialized_goals}


def _shared_notes(session: SessionIdentity, args: SharedNotesArgs) -> dict[str, object]:
    with _connect_orders_database(session) as connection:
        rows = connection.execute(
            "SELECT id, author, text, created_at FROM ordini_shared_notes ORDER BY created_at DESC, id DESC LIMIT ?",
            (args.limit,),
        ).fetchall()

    return {
        "count": len(rows),
        "items": [
            {
                "id": row["id"],
                "author": row["author"],
                "text": row["text"],
                "created_at": _serialize_datetime(row["created_at"]),
            }
            for row in rows
        ],
    }


def _resolve_note_match(
    session: SessionIdentity,
    *,
    note_id: int | None,
    match_text: str,
) -> tuple[str, sqlite3.Row | None, list[dict[str, object]]]:
    with _connect_orders_database(session) as connection:
        if note_id is not None:
            row = connection.execute(
                "SELECT id, author, text, created_at FROM ordini_shared_notes WHERE id = ?",
                (note_id,),
            ).fetchone()
            if row is None:
                return "not_found", None, []
            return "matched", row, [{"id": row["id"], "author": row["author"], "text": row["text"], "created_at": _serialize_datetime(row["created_at"])}]

        cleaned_match = match_text.strip()
        if not cleaned_match:
            return "clarification_required", None, []

        rows = connection.execute(
            "SELECT id, author, text, created_at FROM ordini_shared_notes ORDER BY created_at DESC, id DESC LIMIT 25"
        ).fetchall()

    scored: list[tuple[float, sqlite3.Row]] = []
    for row in rows:
        score = _score_text_match(cleaned_match, str(row["text"]))
        if score <= 0:
            continue
        scored.append((score, row))

    if not scored:
        return "not_found", None, []

    scored.sort(key=lambda entry: (-entry[0], -int(entry[1]["id"])))
    candidates = [
        {"id": row["id"], "author": row["author"], "text": row["text"], "created_at": _serialize_datetime(row["created_at"])}
        for _, row in scored
    ]
    if len(scored) == 1 or scored[0][0] >= scored[1][0] + 2.0:
        return "matched", scored[0][1], candidates
    return "ambiguous", None, candidates


def _write_shared_note(session: SessionIdentity, args: SharedNoteWriteArgs) -> dict[str, object]:
    author = _clean_optional_text(args.author) or _default_staff(session)
    if args.operation == "create":
        text = _clean_optional_text(args.text)
        if not text:
            return {"status": "clarification_required", "detail": "Dimmi il testo della nota da salvare."}
        with _connect_orders_database(session) as connection:
            cursor = connection.execute(
                "INSERT INTO ordini_shared_notes (author, text) VALUES (?, ?)",
                (author, text),
            )
            connection.commit()
            note_id = int(cursor.lastrowid)
            row = connection.execute(
                "SELECT id, author, text, created_at FROM ordini_shared_notes WHERE id = ?",
                (note_id,),
            ).fetchone()
        return {
            "status": "created",
            "note": {
                "id": row["id"],
                "author": row["author"],
                "text": row["text"],
                "created_at": _serialize_datetime(row["created_at"]),
            },
        }

    status, row, candidates = _resolve_note_match(session, note_id=args.note_id, match_text=args.match_text)
    if status == "clarification_required":
        return {"status": "clarification_required", "detail": "Indicami quale nota vuoi modificare o eliminare."}
    if status == "not_found":
        return {"status": "not_found", "detail": "Non trovo una nota che corrisponda alla tua richiesta."}
    if status == "ambiguous":
        return {
            "status": "clarification_required",
            "detail": "Ho trovato piu note compatibili. Indicami meglio quale vuoi toccare.",
            "candidates": candidates,
        }

    assert row is not None
    with _connect_orders_database(session) as connection:
        if args.operation == "delete":
            connection.execute("DELETE FROM ordini_shared_notes WHERE id = ?", (row["id"],))
            connection.commit()
            return {
                "status": "deleted",
                "note": {
                    "id": row["id"],
                    "author": row["author"],
                    "text": row["text"],
                    "created_at": _serialize_datetime(row["created_at"]),
                },
            }

        text = _clean_optional_text(args.text)
        if not text:
            return {"status": "clarification_required", "detail": "Dimmi il nuovo testo della nota."}
        connection.execute(
            "UPDATE ordini_shared_notes SET author = ?, text = ? WHERE id = ?",
            (author, text, row["id"]),
        )
        connection.commit()
        updated = connection.execute(
            "SELECT id, author, text, created_at FROM ordini_shared_notes WHERE id = ?",
            (row["id"],),
        ).fetchone()

    return {
        "status": "updated",
        "note": {
            "id": updated["id"],
            "author": updated["author"],
            "text": updated["text"],
            "created_at": _serialize_datetime(updated["created_at"]),
        },
    }


def _resolve_goal_match(
    session: SessionIdentity,
    *,
    goal_id: int | None,
    year: int | None,
    name: str | None,
) -> tuple[str, sqlite3.Row | None, list[dict[str, object]]]:
    with _connect_orders_database(session) as connection:
        if goal_id is not None:
            row = connection.execute("SELECT * FROM ordini_seasonal_goals WHERE id = ?", (goal_id,)).fetchone()
            if row is None:
                return "not_found", None, []
            return "matched", row, [{"id": row["id"], "year": row["year"], "name": row["name"], "goal_type": row["goal_type"]}]

        cleaned_name = _clean_optional_text(name)
        if not cleaned_name:
            return "clarification_required", None, []

        params: list[object] = []
        query = "SELECT * FROM ordini_seasonal_goals"
        if year is not None:
            query += " WHERE year = ?"
            params.append(year)
        rows = connection.execute(f"{query} ORDER BY year DESC, id DESC", params).fetchall()

    scored: list[tuple[float, sqlite3.Row]] = []
    cleaned_anchor = _goal_name_anchor(cleaned_name)
    for row in rows:
        row_name = str(row["name"])
        row_anchor = _goal_name_anchor(row_name)
        if cleaned_anchor and row_anchor and _score_text_match(cleaned_anchor, row_anchor) <= 0:
            continue
        score = _score_text_match(cleaned_name, row_name)
        if score <= 0:
            continue
        scored.append((score, row))

    if not scored:
        return "not_found", None, []

    scored.sort(key=lambda entry: (-entry[0], -int(entry[1]["year"]), -int(entry[1]["id"])))
    candidates = [{"id": row["id"], "year": row["year"], "name": row["name"], "goal_type": row["goal_type"]} for _, row in scored]
    if len(scored) == 1 or scored[0][0] >= scored[1][0] + 2.0:
        return "matched", scored[0][1], candidates
    return "ambiguous", None, candidates


def _write_sales_goal(session: SessionIdentity, args: SalesGoalWriteArgs) -> dict[str, object]:
    target_year = args.year or _today_in_timezone().year
    cleaned_name = _clean_optional_text(args.name)
    if cleaned_name is None:
        cleaned_name = _build_default_sales_goal_name(
            {
                "supplier_match": args.supplier_match,
                "product_match": args.product_match,
                "description": args.description,
                "year": target_year,
            }
        )
    match_status, matched_goal, candidates = _resolve_goal_match(
        session,
        goal_id=args.goal_id,
        year=target_year if args.year is not None else None,
        name=cleaned_name,
    )

    if args.operation == "delete":
        if match_status == "clarification_required":
            return {"status": "clarification_required", "detail": "Indicami il nome o l'id dell'obiettivo da eliminare."}
        if match_status == "not_found":
            return {"status": "not_found", "detail": "Non trovo un obiettivo corrispondente da eliminare."}
        if match_status == "ambiguous":
            return {
                "status": "clarification_required",
                "detail": "Ho trovato piu obiettivi compatibili. Dimmi quale vuoi eliminare.",
                "candidates": candidates,
            }

        assert matched_goal is not None
        with _connect_orders_database(session) as connection:
            connection.execute("DELETE FROM ordini_seasonal_goals WHERE id = ?", (matched_goal["id"],))
            connection.commit()
        return {
            "status": "deleted",
            "goal": {
                "id": matched_goal["id"],
                "year": matched_goal["year"],
                "name": matched_goal["name"],
                "goal_type": matched_goal["goal_type"],
            },
        }

    goal_type = args.goal_type or ("note" if _clean_optional_text(args.description) and args.target is None else None)
    if goal_type is None and matched_goal is None:
        goal_type = "quantity"

    if matched_goal is None and not cleaned_name:
        return {"status": "clarification_required", "detail": "Per salvare un obiettivo mi serve almeno il nome dell'obiettivo."}

    if goal_type in {"quantity", "liters"} and args.target is None and matched_goal is None:
        return {"status": "clarification_required", "detail": "Per questo obiettivo mi serve anche il target numerico."}
    if goal_type == "liters_dual" and (args.target is None or args.secondary_target is None or not _clean_optional_text(args.secondary_product_match)):
        return {
            "status": "clarification_required",
            "detail": "Per un doppio target mi servono target primario, target secondario e il secondo prodotto da tracciare.",
        }
    if goal_type in {"quantity", "liters", "liters_dual"} and not (
        _clean_optional_text(args.product_match)
        or _clean_optional_text(args.supplier_match)
        or (matched_goal is not None and (matched_goal["product_match"] or matched_goal["supplier_match"]))
    ):
        return {
            "status": "clarification_required",
            "detail": "Per un obiettivo numerico indicami almeno il prodotto o il fornitore da monitorare.",
        }

    if match_status == "ambiguous":
        return {
            "status": "clarification_required",
            "detail": "Ho trovato piu obiettivi compatibili. Dimmi meglio quale vuoi aggiornare.",
            "candidates": candidates,
        }

    fields = {
        "year": target_year,
        "name": cleaned_name or (matched_goal["name"] if matched_goal is not None else None),
        "goal_type": goal_type or (matched_goal["goal_type"] if matched_goal is not None else "quantity"),
        "description": _clean_optional_text(args.description) if "description" in args.model_fields_set else (matched_goal["description"] if matched_goal is not None else None),
        "product_match": _clean_optional_text(args.product_match) if "product_match" in args.model_fields_set else (matched_goal["product_match"] if matched_goal is not None else None),
        "secondary_product_match": _clean_optional_text(args.secondary_product_match) if "secondary_product_match" in args.model_fields_set else (matched_goal["secondary_product_match"] if matched_goal is not None else None),
        "supplier_match": _clean_optional_text(args.supplier_match) if "supplier_match" in args.model_fields_set else (matched_goal["supplier_match"] if matched_goal is not None else None),
        "target": args.target if "target" in args.model_fields_set else (matched_goal["target"] if matched_goal is not None else None),
        "secondary_target": args.secondary_target if "secondary_target" in args.model_fields_set else (matched_goal["secondary_target"] if matched_goal is not None else None),
        "unit_label": _clean_optional_text(args.unit_label) if "unit_label" in args.model_fields_set else (matched_goal["unit_label"] if matched_goal is not None else None),
        "bonus_label": _clean_optional_text(args.bonus_label) if "bonus_label" in args.model_fields_set else (matched_goal["bonus_label"] if matched_goal is not None else None),
    }

    with _connect_orders_database(session) as connection:
        if fields["goal_type"] in {"liters", "liters_dual"}:
            missing_pack_sizes = _find_liters_goal_missing_pack_sizes(
                connection,
                supplier_match=_clean_optional_text(fields["supplier_match"]),
                product_match=_clean_optional_text(fields["product_match"]),
                secondary_product_match=_clean_optional_text(fields["secondary_product_match"]),
            )
            if missing_pack_sizes:
                preview = ", ".join(
                    f"{item['product_name']} ({item['lot_code']}, {item['supplier_name']})" for item in missing_pack_sizes
                )
                return {
                    "status": "clarification_required",
                    "detail": (
                        "Per contare bene i litri mi serve sapere quante unita contiene il lotto "
                        "cartone/cassa di questi prodotti: "
                        f"{preview}. "
                        "Aggiorna questo dato dal catalogo prodotti oppure dimmelo e poi salvo l'obiettivo."
                    ),
                    "missing_products": missing_pack_sizes,
                }

        if matched_goal is None:
            cursor = connection.execute(
                """
                INSERT INTO ordini_seasonal_goals (
                    year, name, goal_type, description, product_match, secondary_product_match,
                    supplier_match, target, secondary_target, unit_label, bonus_label
                ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                """,
                (
                    fields["year"],
                    fields["name"],
                    fields["goal_type"],
                    fields["description"],
                    fields["product_match"],
                    fields["secondary_product_match"],
                    fields["supplier_match"],
                    fields["target"],
                    fields["secondary_target"],
                    fields["unit_label"],
                    fields["bonus_label"],
                ),
            )
            connection.commit()
            goal_id = int(cursor.lastrowid)
            row = connection.execute("SELECT * FROM ordini_seasonal_goals WHERE id = ?", (goal_id,)).fetchone()
            status_value = "created"
        else:
            connection.execute(
                """
                UPDATE ordini_seasonal_goals
                SET year = ?, name = ?, goal_type = ?, description = ?, product_match = ?,
                    secondary_product_match = ?, supplier_match = ?, target = ?, secondary_target = ?,
                    unit_label = ?, bonus_label = ?
                WHERE id = ?
                """,
                (
                    fields["year"],
                    fields["name"],
                    fields["goal_type"],
                    fields["description"],
                    fields["product_match"],
                    fields["secondary_product_match"],
                    fields["supplier_match"],
                    fields["target"],
                    fields["secondary_target"],
                    fields["unit_label"],
                    fields["bonus_label"],
                    matched_goal["id"],
                ),
            )
            connection.commit()
            row = connection.execute("SELECT * FROM ordini_seasonal_goals WHERE id = ?", (matched_goal["id"],)).fetchone()
            status_value = "updated"

    return {
        "status": status_value,
        "goal": {
            "id": row["id"],
            "year": row["year"],
            "name": row["name"],
            "goal_type": row["goal_type"],
            "description": row["description"],
            "product_match": row["product_match"],
            "secondary_product_match": row["secondary_product_match"],
            "supplier_match": row["supplier_match"],
            "target": row["target"],
            "secondary_target": row["secondary_target"],
            "unit_label": row["unit_label"],
            "bonus_label": row["bonus_label"],
        },
    }


def _time_window_matches(raw_time: str, time_window: str) -> bool:
    try:
        reservation_time = time.fromisoformat(raw_time[:5] if len(raw_time) >= 5 else raw_time)
    except ValueError:
        return True

    if time_window == "lunch":
        return reservation_time < time(17, 0)
    if time_window == "evening":
        return reservation_time >= time(18, 0)
    return True


def _reservation_item_start_time(item: dict[str, object]) -> time | None:
    raw_time = _format_clock(item.get("start_time"))
    if not raw_time:
        return None
    try:
        return time.fromisoformat(raw_time)
    except ValueError:
        return None


def _reservation_overlaps_target_time(item: dict[str, object], target_time: time | None) -> bool:
    if target_time is None:
        return True
    start_time = _reservation_item_start_time(item)
    if start_time is None:
        return False
    duration_minutes = int(item.get("duration_minutes") or 0)
    if duration_minutes <= 0:
        duration_minutes = 120
    target_minutes = target_time.hour * 60 + target_time.minute
    start_minutes = start_time.hour * 60 + start_time.minute
    end_minutes = start_minutes + duration_minutes
    return start_minutes <= target_minutes < end_minutes


def _reservation_room_id(item: dict[str, object]) -> int | None:
    room_id = item.get("room_id")
    if isinstance(room_id, int):
        return int(room_id)
    if isinstance(room_id, str):
        raw_room_id = room_id.strip()
        if raw_room_id.isdigit():
            return int(raw_room_id)

    assigned_table = item.get("assigned_table") if isinstance(item.get("assigned_table"), dict) else None
    if assigned_table and isinstance(assigned_table.get("room_id"), int):
        return int(assigned_table["room_id"])

    assigned_combination = item.get("assigned_combination") if isinstance(item.get("assigned_combination"), dict) else None
    if assigned_combination and isinstance(assigned_combination.get("room_id"), int):
        return int(assigned_combination["room_id"])

    return None


async def _prenotazioni_request(
    session: SessionIdentity,
    *,
    method: str,
    path: str,
    params: dict[str, object] | None = None,
    json_body: dict[str, object] | None = None,
) -> dict[str, object] | list[object]:
    settings = get_settings()
    base_url = settings.assistant_prenotazioni_internal_url.rstrip("/")
    try:
        async with httpx.AsyncClient(timeout=httpx.Timeout(settings.assistant_data_request_timeout_seconds, connect=5.0)) as client:
            response = await client.request(
                method.upper(),
                f"{base_url}{path}",
                params=params,
                json=json_body,
                headers={"Authorization": f"Bearer {session.token}"},
            )
            response.raise_for_status()
    except httpx.HTTPError as exc:
        raise HTTPException(
            status_code=502,
            detail=f"Impossibile contattare il modulo prenotazioni: {_extract_http_error_detail(exc)}",
        ) from exc

    payload = response.json() if response.content else {}
    if isinstance(payload, (dict, list)):
        return payload
    return {}


async def _get_prenotazioni_venue_id(session: SessionIdentity) -> int:
    payload = await _prenotazioni_request(session, method="GET", path="/booking-settings")
    if not isinstance(payload, dict) or not isinstance(payload.get("venue_id"), int):
        raise HTTPException(status_code=502, detail="Il modulo prenotazioni non ha restituito un venue_id valido")
    return int(payload["venue_id"])


def _serialize_reservation_brief(item: dict[str, object]) -> dict[str, object]:
    customer = item.get("customer") if isinstance(item.get("customer"), dict) else {}
    assigned_table = item.get("assigned_table") if isinstance(item.get("assigned_table"), dict) else {}
    assigned_combination = item.get("assigned_combination") if isinstance(item.get("assigned_combination"), dict) else {}
    return {
        "id": item.get("id"),
        "customer_name": str(customer.get("name") or "").strip(),
        "customer_phone": customer.get("phone"),
        "customer_email": customer.get("email"),
        "reservation_date": item.get("reservation_date"),
        "start_time": _format_clock(item.get("start_time")),
        "guests": item.get("guests"),
        "status": item.get("status"),
        "source": item.get("source"),
        "notes": item.get("notes"),
        "area_preference": item.get("area_preference"),
        "assignment": assigned_table.get("name") or assigned_combination.get("name"),
    }


async def _list_reservations_raw(session: SessionIdentity, *, target_date: date | None = None) -> list[dict[str, object]]:
    params = {"reservation_date": target_date.isoformat()} if target_date is not None else None
    payload = await _prenotazioni_request(session, method="GET", path="/reservations", params=params)
    if not isinstance(payload, dict):
        return []
    raw_items = payload.get("items")
    if not isinstance(raw_items, list):
        return []
    return [item for item in raw_items if isinstance(item, dict)]


def _build_reservation_candidate_entry(item: dict[str, object], score: float) -> dict[str, object]:
    serialized = _serialize_reservation_brief(item)
    serialized["match_score"] = round(score, 3)
    return serialized


async def _resolve_reservation_match(
    session: SessionIdentity,
    *,
    reservation_id: int | None,
    customer_query: str,
    customer_phone: str | None,
    target_date: date | None,
    target_time: time | None,
    allow_inactive: bool = False,
) -> tuple[str, dict[str, object] | None, list[dict[str, object]]]:
    if reservation_id is not None:
        payload = await _prenotazioni_request(session, method="GET", path=f"/reservations/{reservation_id}")
        if not isinstance(payload, dict):
            return "not_found", None, []
        return "matched", payload, [_build_reservation_candidate_entry(payload, 99.0)]

    if not customer_query.strip() and not _phone_digits(customer_phone) and target_date is None and target_time is None:
        return "clarification_required", None, []

    raw_items = await _list_reservations_raw(session, target_date=target_date)
    if target_date is None:
        raw_items.sort(
            key=lambda item: (
                str(item.get("reservation_date") or ""),
                _format_clock(item.get("start_time")) or "",
            )
        )

    target_time_text = _format_clock(target_time)
    target_phone_digits = _phone_digits(customer_phone)
    scored: list[tuple[float, dict[str, object]]] = []
    for item in raw_items:
        status_value = str(item.get("status") or "")
        if not allow_inactive and status_value in {"cancelled", "no_show", "completed"}:
            continue

        brief = _serialize_reservation_brief(item)
        score = 0.0
        if customer_query.strip():
            searchable = f"{brief['customer_name']} {brief['customer_phone'] or ''}"
            score = _score_text_match(customer_query, searchable)
            if score <= 0:
                continue

        if target_phone_digits:
            candidate_phone_digits = _phone_digits(str(brief.get("customer_phone") or ""))
            if not candidate_phone_digits or not (
                candidate_phone_digits.endswith(target_phone_digits) or target_phone_digits.endswith(candidate_phone_digits)
            ):
                continue
            score += 5.0

        if target_time_text:
            if (brief.get("start_time") or "") != target_time_text:
                continue
            score += 3.0

        if target_date is not None and str(brief.get("reservation_date") or "") == target_date.isoformat():
            score += 2.0

        scored.append((score, item))

    if not scored:
        return "not_found", None, []

    scored.sort(
        key=lambda entry: (
            -entry[0],
            str(entry[1].get("reservation_date") or ""),
            _format_clock(entry[1].get("start_time")) or "",
            str(((entry[1].get("customer") or {}) if isinstance(entry[1].get("customer"), dict) else {}).get("name") or "").lower(),
        )
    )

    candidates = [_build_reservation_candidate_entry(item, score) for score, item in scored]
    if len(scored) == 1:
        return "matched", scored[0][1], candidates

    top_score = scored[0][0]
    second_score = scored[1][0]
    if top_score >= second_score + 2.0:
        return "matched", scored[0][1], candidates

    return "ambiguous", None, candidates


async def _create_reservation(session: SessionIdentity, args: ReservationCreateArgs) -> dict[str, object]:
    missing_fields: list[str] = []
    if not _clean_optional_text(args.customer_name):
        missing_fields.append("nome cliente")
    if not _clean_optional_text(args.customer_phone):
        missing_fields.append("telefono cliente")
    if args.reservation_date is None:
        missing_fields.append("data")
    if args.start_time is None:
        missing_fields.append("orario")
    if args.guests is None:
        missing_fields.append("numero di persone")

    if missing_fields:
        return {
            "status": "clarification_required",
            "missing_fields": missing_fields,
            "detail": f"Per creare la prenotazione mi servono ancora: {', '.join(missing_fields)}.",
        }

    venue_id = await _get_prenotazioni_venue_id(session)
    payload = {
        "venue_id": venue_id,
        "customer_name": _clean_optional_text(args.customer_name),
        "customer_phone": _clean_optional_text(args.customer_phone),
        "customer_email": _clean_optional_text(args.customer_email),
        "reservation_date": args.reservation_date.isoformat(),
        "start_time": _format_clock(args.start_time),
        "duration_minutes": args.duration_minutes,
        "guests": args.guests,
        "status": args.status,
        "source": args.source,
        "notes": _clean_optional_text(args.notes),
        "area_preference": _clean_optional_text(args.area_preference),
    }

    try:
        result = await _prenotazioni_request(session, method="POST", path="/reservations", json_body=payload)
    except HTTPException as exc:
        return {"status": "validation_error", "detail": exc.detail if isinstance(exc.detail, str) else "Errore validazione prenotazione"}

    if not isinstance(result, dict):
        return {"status": "validation_error", "detail": "Risposta non valida dal modulo prenotazioni"}
    return {"status": "created", "reservation": _serialize_reservation_brief(result)}


async def _update_reservation(session: SessionIdentity, args: ReservationUpdateArgs) -> dict[str, object]:
    status, matched, candidates = await _resolve_reservation_match(
        session,
        reservation_id=args.reservation_id,
        customer_query=args.customer_query,
        customer_phone=args.customer_phone,
        target_date=args.target_date,
        target_time=args.target_time,
    )
    if status == "clarification_required":
        return {
            "status": "clarification_required",
            "detail": "Per capire quale prenotazione modificare mi serve almeno nome cliente, telefono, data oppure orario.",
        }
    if status == "not_found":
        return {"status": "not_found", "detail": "Non trovo una prenotazione attiva che corrisponda ai dati indicati."}
    if status == "ambiguous":
        return {
            "status": "clarification_required",
            "detail": "Ho trovato piu prenotazioni compatibili. Indicami quale vuoi modificare con nome, data, orario o telefono.",
            "candidates": candidates,
        }

    assert matched is not None
    changes: dict[str, object] = {}
    if "new_customer_name" in args.model_fields_set:
        changes["customer_name"] = _clean_optional_text(args.new_customer_name)
    if "new_customer_phone" in args.model_fields_set:
        changes["customer_phone"] = _clean_optional_text(args.new_customer_phone)
    if "new_customer_email" in args.model_fields_set:
        changes["customer_email"] = _clean_optional_text(args.new_customer_email)
    if "new_reservation_date" in args.model_fields_set and args.new_reservation_date is not None:
        changes["reservation_date"] = args.new_reservation_date.isoformat()
    if "new_start_time" in args.model_fields_set and args.new_start_time is not None:
        changes["start_time"] = _format_clock(args.new_start_time)
    if "new_duration_minutes" in args.model_fields_set:
        changes["duration_minutes"] = args.new_duration_minutes
    if "new_guests" in args.model_fields_set:
        changes["guests"] = args.new_guests
    if "new_notes" in args.model_fields_set:
        changes["notes"] = _clean_optional_text(args.new_notes)
    if "new_area_preference" in args.model_fields_set:
        changes["area_preference"] = _clean_optional_text(args.new_area_preference)
    if "new_status" in args.model_fields_set:
        changes["status"] = args.new_status

    if not changes:
        return {"status": "clarification_required", "detail": "Dimmi cosa vuoi cambiare nella prenotazione."}

    try:
        result = await _prenotazioni_request(
            session,
            method="PUT",
            path=f"/reservations/{matched.get('id')}",
            json_body=changes,
        )
    except HTTPException as exc:
        return {"status": "validation_error", "detail": exc.detail if isinstance(exc.detail, str) else "Errore aggiornamento prenotazione"}

    if not isinstance(result, dict):
        return {"status": "validation_error", "detail": "Risposta non valida dal modulo prenotazioni"}
    return {
        "status": "updated",
        "reservation": _serialize_reservation_brief(result),
        "applied_changes": changes,
    }


async def _delete_reservation(session: SessionIdentity, args: ReservationDeleteArgs) -> dict[str, object]:
    status, matched, candidates = await _resolve_reservation_match(
        session,
        reservation_id=args.reservation_id,
        customer_query=args.customer_query,
        customer_phone=args.customer_phone,
        target_date=args.target_date,
        target_time=args.target_time,
    )
    if status == "clarification_required":
        return {
            "status": "clarification_required",
            "detail": "Per capire quale prenotazione eliminare mi serve almeno nome cliente, telefono, data oppure orario.",
        }
    if status == "not_found":
        return {"status": "not_found", "detail": "Non trovo una prenotazione attiva che corrisponda ai dati indicati."}
    if status == "ambiguous":
        return {
            "status": "clarification_required",
            "detail": "Ho trovato piu prenotazioni compatibili. Indicami quale vuoi eliminare con nome, data, orario o telefono.",
            "candidates": candidates,
        }

    assert matched is not None
    try:
        result = await _prenotazioni_request(session, method="DELETE", path=f"/reservations/{matched.get('id')}")
    except HTTPException as exc:
        return {"status": "validation_error", "detail": exc.detail if isinstance(exc.detail, str) else "Errore eliminazione prenotazione"}

    if not isinstance(result, dict):
        return {"status": "validation_error", "detail": "Risposta non valida dal modulo prenotazioni"}
    return {
        "status": "deleted",
        "reservation": _serialize_reservation_brief(matched),
        "detail": result.get("detail"),
    }


async def _reservations_snapshot(session: SessionIdentity, args: ReservationsSnapshotArgs) -> dict[str, object]:
    target_day = args.target_date or _today_in_timezone()
    target_date = target_day.isoformat()
    customer_query = args.customer_query.strip()
    target_time_text = args.target_time.strftime("%H:%M") if args.target_time is not None else None

    payload = await _prenotazioni_request(
        session,
        method="GET",
        path="/reservations",
        params={"reservation_date": target_date},
    )
    raw_items = payload.get("items", []) if isinstance(payload, dict) else []
    raw_rooms = await _prenotazioni_request(session, method="GET", path="/rooms")
    rooms = [room for room in raw_rooms if isinstance(room, dict)]
    room_summaries: list[dict[str, object]] = []
    room_names_by_id = {
        int(room["id"]): str(room.get("name") or "").strip()
        for room in rooms
        if isinstance(room.get("id"), int)
    }

    items: list[dict[str, object]] = []
    for item in raw_items:
        if not isinstance(item, dict):
            continue
        start_time = str(item.get("start_time") or "")
        customer = item.get("customer") if isinstance(item.get("customer"), dict) else {}
        customer_name = str(customer.get("name") or "").strip()
        searchable = f"{customer_name} {customer.get('phone') or ''}"
        if customer_query and _score_text_match(customer_query, searchable) <= 0:
            continue
        if not _time_window_matches(start_time, args.time_window):
            continue
        room_id = _reservation_room_id(item)
        items.append(
            {
                "id": item.get("id"),
                "customer_name": customer_name,
                "customer_phone": customer.get("phone"),
                "start_time": start_time,
                "guests": item.get("guests"),
                "duration_minutes": item.get("duration_minutes"),
                "status": item.get("status"),
                "source": item.get("source"),
                "room_id": room_id,
                "room_name": room_names_by_id.get(room_id),
                "assignment": (
                    (item.get("assigned_table") or {}).get("name")
                    if isinstance(item.get("assigned_table"), dict)
                    else None
                )
                or (
                    (item.get("assigned_combination") or {}).get("name")
                    if isinstance(item.get("assigned_combination"), dict)
                    else None
                ),
                "notes": item.get("notes"),
            }
        )

    items.sort(key=lambda entry: str(entry["start_time"]))
    limited_items = items[: args.limit]
    status_counter = Counter(str(item.get("status") or "") for item in items)
    total_guests = sum(int(item.get("guests") or 0) for item in items)

    slot_items = [
        item for item in items
        if _reservation_overlaps_target_time(item, args.target_time)
    ]
    slot_guest_total = sum(int(item.get("guests") or 0) for item in slot_items)

    for room in rooms:
        room_id = room.get("id")
        if not isinstance(room_id, int):
            continue
        floor_plan_payload = await _prenotazioni_request(
            session,
            method="GET",
            path="/floor-plan",
            params={"date": target_date, "room_id": room_id},
        )
        if not isinstance(floor_plan_payload, dict):
            continue

        room_tables = floor_plan_payload.get("tables") if isinstance(floor_plan_payload.get("tables"), list) else []
        table_states = floor_plan_payload.get("table_states") if isinstance(floor_plan_payload.get("table_states"), list) else []
        room_reservations = [item for item in items if _reservation_room_id(item) == room_id]
        overlapping_room_reservations = [item for item in room_reservations if _reservation_overlaps_target_time(item, args.target_time)]

        active_tables = [
            table for table in room_tables
            if isinstance(table, dict) and bool(table.get("is_active", True))
        ]
        max_capacity = sum(int(table.get("max_seats") or 0) for table in active_tables)
        overlapping_guest_total = sum(int(item.get("guests") or 0) for item in overlapping_room_reservations)
        occupied_table_ids: set[int] = set()
        if args.target_time is not None:
            for state in table_states:
                if not isinstance(state, dict):
                    continue
                occupancy_windows = state.get("occupancy_windows") if isinstance(state.get("occupancy_windows"), list) else []
                for window in occupancy_windows:
                    if not isinstance(window, dict):
                        continue
                    try:
                        start_value = time.fromisoformat(str(window.get("start_time") or "")[:5])
                        end_value = time.fromisoformat(str(window.get("end_time") or "")[:5])
                    except ValueError:
                        continue
                    if start_value <= args.target_time < end_value:
                        if isinstance(state.get("table_id"), int):
                            occupied_table_ids.add(int(state["table_id"]))
                        break
        else:
            occupied_table_ids = {
                int(state["table_id"])
                for state in table_states
                if isinstance(state, dict) and isinstance(state.get("table_id"), int) and bool(state.get("occupancy_windows"))
            }

        room_summaries.append(
            {
                "room_id": room_id,
                "room_name": str(room.get("name") or "").strip(),
                "table_count": len(active_tables),
                "max_capacity": max_capacity,
                "assigned_guests": overlapping_guest_total if args.target_time is not None else sum(int(item.get("guests") or 0) for item in room_reservations),
                "available_seats": max(max_capacity - (overlapping_guest_total if args.target_time is not None else sum(int(item.get("guests") or 0) for item in room_reservations)), 0),
                "occupied_tables": len(occupied_table_ids),
                "free_tables": max(len(active_tables) - len(occupied_table_ids), 0),
                "reservation_count": len(overlapping_room_reservations) if args.target_time is not None else len(room_reservations),
            }
        )

    return {
        "target_date": target_date,
        "target_time": target_time_text,
        "time_window": args.time_window,
        "customer_query": customer_query,
        "total_reservations": len(items),
        "total_guests": total_guests,
        "slot_guest_total": slot_guest_total,
        "status_breakdown": dict(status_counter),
        "room_summaries": room_summaries,
        "items": limited_items,
    }


def _normalize_document_title(raw_title: str | None, *, kind: Literal["doc", "sheet"]) -> str:
    cleaned = re.sub(r"\s+", " ", str(raw_title or "").strip())
    if cleaned:
        return cleaned[:200]
    timestamp = _now_in_timezone().strftime("%Y-%m-%d %H:%M")
    prefix = "Foglio operativo" if kind == "sheet" else "Documento operativo"
    return f"{prefix} {timestamp}"


def _compose_document_generation_prompt(
    *,
    base_prompt: str,
    prior_tool_results: list[dict[str, object]],
) -> str:
    prompt = base_prompt.strip()
    if not prior_tool_results:
        return prompt
    return "\n\n".join(
        [
            prompt,
            "Usa SOLO i dati reali qui sotto per costruire il documento. Se un dato non e' presente, non inventarlo.",
            json.dumps(prior_tool_results, ensure_ascii=False, default=str),
        ]
    )


def _format_purchase_result_scope(result: dict[str, object]) -> str:
    year = result.get("year")
    month = result.get("month")
    month_label = _format_italian_month(month if isinstance(month, int) else None)
    if month_label and year:
        return f" per {month_label} {year}"
    if month_label:
        return f" per {month_label}"
    if year:
        return f" nel {year}"
    return ""


def _stringify_query_preview_cell(value: object) -> str:
    if value is None:
        return ""
    if isinstance(value, float):
        if value.is_integer():
            return str(int(value))
        return f"{value:.4f}".rstrip("0").rstrip(".")
    return str(value)


def _build_tenant_query_document_preview(
    *,
    args: GoogleWorkspaceDocumentArgs,
    result: dict[str, object],
) -> GoogleWorkspaceDocumentPreview:
    raw_columns = result.get("columns") if isinstance(result.get("columns"), list) else []
    columns = [str(column) for column in raw_columns if str(column).strip()]
    raw_rows = result.get("rows") if isinstance(result.get("rows"), list) else []
    rows = [row for row in raw_rows if isinstance(row, dict)]
    if not columns and rows:
        columns = [str(key) for key in rows[0].keys()]

    title = _normalize_document_title(args.title, kind=args.kind)
    row_count = int(result.get("row_count") or len(rows))
    summary = f"Anteprima basata su {row_count} righe reali lette dal tenant."

    if args.kind == "sheet":
        effective_columns = columns or ["risultato"]
        effective_rows = [
            [_stringify_query_preview_cell(row.get(column)) for column in effective_columns]
            for row in rows
        ]
        return GoogleWorkspaceDocumentPreview(
            kind="sheet",
            title=title,
            summary=summary,
            destination_folder_id=args.destination_folder_id,
            columns=effective_columns,
            rows=effective_rows,
        )

    heading = (args.prompt or "Risultati reali del tenant").strip(" :")
    lines = [f"{heading}:"]
    if not rows:
        lines.append("- Nessuna riga trovata.")
    else:
        for row in rows:
            rendered_cells = []
            for column in columns:
                cell = _stringify_query_preview_cell(row.get(column))
                if cell:
                    rendered_cells.append(f"{column}: {cell}")
            lines.append(f"- {' | '.join(rendered_cells) if rendered_cells else 'Riga vuota'}")

    return GoogleWorkspaceDocumentPreview(
        kind="doc",
        title=title,
        summary=summary,
        destination_folder_id=args.destination_folder_id,
        content="\n".join(lines),
    )


def _preview_cell_from_tool_value(value: object) -> str:
    if value is None:
        return ""
    if isinstance(value, (str, int, float, bool)):
        return _stringify_query_preview_cell(value)
    return json.dumps(value, ensure_ascii=False, default=str)


def _rows_from_tool_result_payload(result: dict[str, object]) -> list[dict[str, object]]:
    preferred_keys = (
        "items",
        "products",
        "batches",
        "reservations",
        "documents",
        "users",
        "entries",
        "goals",
        "notes",
        "recipes",
    )
    for key in preferred_keys:
        value = result.get(key)
        if isinstance(value, list):
            rows: list[dict[str, object]] = []
            for item in value:
                if isinstance(item, dict):
                    rows.append(item)
                else:
                    rows.append({"value": item})
            return rows

    scalar_row = {
        str(key): value
        for key, value in result.items()
        if not isinstance(value, (dict, list))
    }
    return [scalar_row] if scalar_row else []


def _build_generic_tool_results_document_preview(
    *,
    args: GoogleWorkspaceDocumentArgs,
    prior_tool_results: list[dict[str, object]],
) -> GoogleWorkspaceDocumentPreview | None:
    sections: list[tuple[str, list[dict[str, object]]]] = []
    for tool_result in prior_tool_results:
        if not isinstance(tool_result, dict):
            continue
        tool_name = str(tool_result.get("tool") or "").strip()
        if tool_name in {"describe_tenant_schema", "create_google_workspace_document"}:
            continue
        result = tool_result.get("result") if isinstance(tool_result.get("result"), dict) else None
        if result is None:
            continue
        rows = _rows_from_tool_result_payload(result)
        if rows:
            sections.append((tool_name or "tool", rows))

    if not sections:
        return None

    title = _normalize_document_title(args.title, kind=args.kind)
    total_rows = sum(len(rows) for _, rows in sections)
    summary = f"Anteprima deterministica basata su {total_rows} righe reali restituite dai tool."

    if args.kind == "sheet":
        columns: list[str] = ["source_tool"]
        for _, rows in sections:
            for row in rows:
                for key in row.keys():
                    column = str(key)
                    if column not in columns:
                        columns.append(column)
                    if len(columns) >= 40:
                        break
                if len(columns) >= 40:
                    break
            if len(columns) >= 40:
                break

        rendered_rows: list[list[str]] = []
        for tool_name, rows in sections:
            for row in rows:
                rendered_rows.append(
                    [
                        tool_name if column == "source_tool" else _preview_cell_from_tool_value(row.get(column))
                        for column in columns
                    ]
                )
        return GoogleWorkspaceDocumentPreview(
            kind="sheet",
            title=title,
            summary=summary,
            destination_folder_id=args.destination_folder_id,
            columns=columns,
            rows=rendered_rows,
        )

    lines = [(args.prompt or "Dati reali del tenant").strip(" :")]
    for tool_name, rows in sections:
        lines.append(f"\n{tool_name}:")
        for row in rows:
            cells = [
                f"{key}: {_preview_cell_from_tool_value(value)}"
                for key, value in row.items()
                if _preview_cell_from_tool_value(value)
            ]
            lines.append(f"- {' | '.join(cells) if cells else 'Riga vuota'}")
    return GoogleWorkspaceDocumentPreview(
        kind="doc",
        title=title,
        summary=summary,
        destination_folder_id=args.destination_folder_id,
        content="\n".join(lines),
    )


def _build_purchase_batches_document_preview(
    *,
    args: GoogleWorkspaceDocumentArgs,
    result: dict[str, object],
) -> GoogleWorkspaceDocumentPreview:
    batches = result.get("batches") if isinstance(result.get("batches"), list) else []
    scope = _format_purchase_result_scope(result)
    title = _normalize_document_title(args.title, kind=args.kind)

    if args.kind == "sheet":
        columns = [
            "Ordine",
            "Data",
            "Operatore",
            "Fornitori ordine",
            "Righe ordine",
            "Quantita totale ordine",
            "Prodotto",
            "Fornitore riga",
            "Formato",
            "Quantita",
        ]
        rows: list[list[str]] = []
        for batch in batches:
            if not isinstance(batch, dict):
                continue
            batch_id = str(batch.get("batch_id") or "")
            confirmed_at = str(batch.get("confirmed_at") or "").replace("T", " ")
            staff = str(batch.get("staff") or "").strip()
            supplier_names = batch.get("supplier_names") if isinstance(batch.get("supplier_names"), list) else []
            suppliers_label = ", ".join(str(name) for name in supplier_names if str(name).strip())
            total_lines = str(batch.get("total_lines") or "")
            total_quantity = str(batch.get("total_quantity") or "")
            items = batch.get("items") if isinstance(batch.get("items"), list) else []
            if not items:
                rows.append([batch_id, confirmed_at, staff, suppliers_label, total_lines, total_quantity, "", "", "", ""])
                continue
            for item in items:
                if not isinstance(item, dict):
                    continue
                rows.append(
                    [
                        batch_id,
                        confirmed_at,
                        staff,
                        suppliers_label,
                        total_lines,
                        total_quantity,
                        str(item.get("product_name") or ""),
                        str(item.get("supplier_name") or ""),
                        str(item.get("lot_code") or ""),
                        str(item.get("quantity") or ""),
                    ]
                )
        summary = f"Anteprima basata su {len(batches)} ordini reali del locale{scope}."
        return GoogleWorkspaceDocumentPreview(
            kind="sheet",
            title=title,
            summary=summary,
            destination_folder_id=args.destination_folder_id,
            columns=columns,
            rows=rows,
        )

    lines = [f"Ordini reali del locale{scope}:"] if scope else ["Ordini reali del locale:"]
    for batch in batches:
        if not isinstance(batch, dict):
            continue
        confirmed_at = str(batch.get("confirmed_at") or "").replace("T", " ")
        staff = str(batch.get("staff") or "").strip()
        total_lines = batch.get("total_lines")
        total_quantity = batch.get("total_quantity")
        lines.append(
            f"- Ordine #{batch.get('batch_id')} del {confirmed_at}: {total_lines} righe, quantita totale {total_quantity}, inserito da {staff or 'n/d'}."
        )
        items = batch.get("items") if isinstance(batch.get("items"), list) else []
        for item in items:
            if isinstance(item, dict):
                lines.append(
                    f"  - {item.get('quantity')} x {item.get('product_name')} da {item.get('supplier_name')} ({item.get('lot_code')})"
                )
    return GoogleWorkspaceDocumentPreview(
        kind="doc",
        title=title,
        summary=f"Anteprima basata su {len(batches)} ordini reali del locale{scope}.",
        destination_folder_id=args.destination_folder_id,
        content="\n".join(lines),
    )


def _build_purchase_history_document_preview(
    *,
    args: GoogleWorkspaceDocumentArgs,
    result: dict[str, object],
) -> GoogleWorkspaceDocumentPreview:
    items = result.get("items") if isinstance(result.get("items"), list) else []
    scope = _format_purchase_result_scope(result)
    title = _normalize_document_title(args.title, kind=args.kind)

    if args.kind == "sheet":
        columns = ["Ordine", "Data", "Operatore", "Prodotto", "Fornitore", "Formato", "Quantita"]
        rows = [
            [
                str(item.get("batch_id") or ""),
                str(item.get("confirmed_at") or "").replace("T", " "),
                str(item.get("staff") or ""),
                str(item.get("product_name") or ""),
                str(item.get("supplier_name") or ""),
                str(item.get("lot_code") or ""),
                str(item.get("quantity") or ""),
            ]
            for item in items
            if isinstance(item, dict)
        ]
        return GoogleWorkspaceDocumentPreview(
            kind="sheet",
            title=title,
            summary=f"Anteprima basata su {len(rows)} righe storiche reali del locale{scope}.",
            destination_folder_id=args.destination_folder_id,
            columns=columns,
            rows=rows,
        )

    lines = [f"Righe storiche reali del locale{scope}:"] if scope else ["Righe storiche reali del locale:"]
    for item in items:
        if isinstance(item, dict):
            confirmed_at = str(item.get("confirmed_at") or "").replace("T", " ")
            lines.append(
                f"- {confirmed_at}: {item.get('quantity')} x {item.get('product_name')} da {item.get('supplier_name')} ({item.get('lot_code')})"
            )
    return GoogleWorkspaceDocumentPreview(
        kind="doc",
        title=title,
        summary=f"Anteprima basata su {len(items)} righe storiche reali del locale{scope}.",
        destination_folder_id=args.destination_folder_id,
        content="\n".join(lines),
    )


def _build_grounded_purchase_document_preview_from_prompt(
    session: SessionIdentity,
    args: GoogleWorkspaceDocumentArgs,
) -> GoogleWorkspaceDocumentPreview | None:
    prompt = _clean_optional_text(args.prompt)
    if not prompt:
        return None

    normalized = _normalize_text(prompt)
    if _is_purchase_batch_detail_request(prompt, normalized):
        batch_id = _extract_purchase_batch_id(prompt)
        target_date = _extract_purchase_batch_date(prompt)
        result = _purchase_batches(
            session,
            PurchaseBatchesArgs(
                query="",
                batch_id=batch_id,
                target_date=target_date,
                year=None if batch_id is not None or target_date is not None else _extract_reference_year(prompt),
                month=None if batch_id is not None or target_date is not None else _extract_reference_month(prompt),
                limit=_CHAT_LIST_LIMIT if target_date is not None and batch_id is None else 1,
            ),
        )
        return _build_purchase_batches_document_preview(args=args, result=result)

    if not _is_orders_document_request(prompt, normalized):
        return None

    year = _extract_reference_year(prompt)
    month = _extract_reference_month(prompt)
    week_range = _extract_reference_week_range(prompt)
    start_date = week_range[0] if week_range is not None else None
    end_date = week_range[1] if week_range is not None else None
    purchase_query = _extract_purchase_query(prompt)
    if any(keyword in normalized for keyword in ("righe", "dettaglio", "dettagli", "linee", "totale riga")):
        result = _purchase_history(
            session,
            PurchaseHistoryArgs(
                query=purchase_query,
                year=year,
                month=month,
                start_date=start_date,
                end_date=end_date,
                limit=_CHAT_LIST_LIMIT,
            ),
        )
        return _build_purchase_history_document_preview(args=args, result=result)

    result = _purchase_batches(
        session,
        PurchaseBatchesArgs(
            query=purchase_query,
            year=year,
            month=month,
            start_date=start_date,
            end_date=end_date,
            limit=_CHAT_LIST_LIMIT,
        ),
    )
    return _build_purchase_batches_document_preview(args=args, result=result)


def _build_grounded_workspace_preview_from_tool_results(
    *,
    args: GoogleWorkspaceDocumentArgs,
    prior_tool_results: list[dict[str, object]],
) -> GoogleWorkspaceDocumentPreview | None:
    for tool_result in prior_tool_results:
        if not isinstance(tool_result, dict):
            continue
        tool_name = str(tool_result.get("tool") or "")
        result = tool_result.get("result") if isinstance(tool_result.get("result"), dict) else None
        if result is None:
            continue
        if tool_name == "get_purchase_batches":
            return _build_purchase_batches_document_preview(args=args, result=result)
        if tool_name == "get_purchase_history":
            return _build_purchase_history_document_preview(args=args, result=result)
        if tool_name == "list_tenant_users":
            return _build_tenant_users_document_preview(args=args, result=result)
        if tool_name == "search_products":
            raw_items = result.get("items") if isinstance(result.get("items"), list) else []
            products: list[CatalogProduct] = []
            for item in raw_items:
                if not isinstance(item, dict):
                    continue
                try:
                    products.append(
                        CatalogProduct(
                            id=int(item.get("id") or 0),
                            product_name=str(item.get("product_name") or "").strip(),
                            lot_code=str(item.get("lot_code") or "").strip(),
                            supplier_name=str(item.get("supplier_name") or "").strip(),
                            product_code=str(item.get("product_code")).strip() if item.get("product_code") is not None else None,
                            final_price_vat=float(item.get("final_price_vat")) if item.get("final_price_vat") is not None else None,
                            category=str(item.get("category")).strip() if item.get("category") is not None else None,
                        )
                    )
                except (TypeError, ValueError):
                    continue

            preview_payload = GoogleWorkspacePreviewRequest(
                kind=args.kind,
                title=_normalize_document_title(args.title, kind=args.kind),
                prompt=args.prompt or "",
                destination_folder_id=args.destination_folder_id,
            )
            query = str(result.get("query") or "").strip() or None
            if args.kind == "doc":
                return _build_catalog_doc_preview(preview_payload, products=products, query=query)
            return _build_catalog_sheet_preview(preview_payload, products=products, query=query)
        if tool_name == "run_tenant_query":
            return _build_tenant_query_document_preview(args=args, result=result)
    return None


def _document_grounded_sources_are_empty(prior_tool_results: list[dict[str, object]]) -> bool:
    empty_checks: list[bool] = []
    for tool_result in prior_tool_results:
        if not isinstance(tool_result, dict):
            continue
        tool_name = str(tool_result.get("tool") or "")
        result = tool_result.get("result") if isinstance(tool_result.get("result"), dict) else {}
        if tool_name == "run_tenant_query":
            empty_checks.append(int(result.get("row_count") or 0) == 0)
        elif tool_name == "search_products":
            empty_checks.append(int(result.get("count") or 0) == 0)
        elif tool_name in {"get_purchase_overview", "get_purchase_frequency"}:
            empty_checks.append(int(result.get("matched_count") or result.get("count") or 0) == 0)
        elif tool_name == "get_purchase_history":
            empty_checks.append(int(result.get("total_matches") or result.get("count") or 0) == 0)
        elif tool_name == "get_purchase_batches":
            batches = result.get("batches") if isinstance(result.get("batches"), list) else []
            empty_checks.append(not batches)
        elif tool_name == "list_tenant_users":
            empty_checks.append(int(result.get("count") or 0) == 0)

    return bool(empty_checks) and all(empty_checks)


def _document_prompt_wants_username_only(prompt: str) -> bool:
    normalized = _normalize_text(prompt)
    return "username" in normalized and (
        any(keyword in normalized for keyword in ("solo", "soltanto", "solamente"))
        or "lista degli username" in normalized
        or "elenco degli username" in normalized
    )


def _format_user_permissions(value: object) -> str:
    if not isinstance(value, list):
        return ""
    permissions = [str(item).strip() for item in value if str(item).strip()]
    return ", ".join(permissions)


def _build_tenant_users_document_preview(
    *,
    args: GoogleWorkspaceDocumentArgs,
    result: dict[str, object],
) -> GoogleWorkspaceDocumentPreview:
    users = [user for user in result.get("users", []) if isinstance(user, dict)]
    username_only = _document_prompt_wants_username_only(args.prompt or "")
    title = _normalize_document_title(args.title, kind=args.kind)
    summary = f"Anteprima basata su {len(users)} account reali del locale."

    if args.kind == "sheet":
        if username_only:
            return GoogleWorkspaceDocumentPreview(
                kind="sheet",
                title=title,
                summary=summary,
                destination_folder_id=args.destination_folder_id,
                columns=["Username"],
                rows=[[str(user.get("username") or "").strip()] for user in users],
            )
        return GoogleWorkspaceDocumentPreview(
            kind="sheet",
            title=title,
            summary=summary,
            destination_folder_id=args.destination_folder_id,
            columns=["Nome", "Username", "Email", "Ruolo", "Permessi"],
            rows=[
                [
                    str(user.get("name") or "").strip(),
                    str(user.get("username") or "").strip(),
                    str(user.get("email") or "").strip(),
                    str(user.get("role") or "").strip(),
                    _format_user_permissions(user.get("permissions")),
                ]
                for user in users
            ],
        )

    if username_only:
        lines = ["Username degli account del locale:"]
        lines.extend(f"- {str(user.get('username') or '').strip()}" for user in users)
    else:
        lines = ["Account del locale:"]
        for user in users:
            parts = [
                f"username {str(user.get('username') or '').strip()}",
                f"email {str(user.get('email') or '').strip()}",
                f"ruolo {str(user.get('role') or '').strip()}",
            ]
            permissions = _format_user_permissions(user.get("permissions"))
            if permissions:
                parts.append(f"permessi {permissions}")
            name = str(user.get("name") or user.get("username") or user.get("email") or "Account").strip()
            lines.append(f"- {name}: {'; '.join(part for part in parts if part)}")

    return GoogleWorkspaceDocumentPreview(
        kind="doc",
        title=title,
        summary=summary,
        destination_folder_id=args.destination_folder_id,
        content="\n".join(lines),
    )


def _build_document_request_message_for_tools(prompt: str, kind: Literal["doc", "sheet"]) -> str:
    file_label = "google sheet" if kind == "sheet" else "google doc"
    return f"creami un file {file_label} {prompt}".strip()


def _surface_allows_tool(surface: AssistantSurface, tool_name: str) -> bool:
    return tool_name in list_allowed_tools_for_profile(surface)


def _filter_tool_calls_for_surface(surface: AssistantSurface, tool_calls: list[PlannedToolCall]) -> list[PlannedToolCall]:
    return [tool_call for tool_call in tool_calls if _surface_allows_tool(surface, tool_call.tool)]


def _normalize_home_planned_tool_calls(message: str, tool_calls: list[PlannedToolCall]) -> list[PlannedToolCall]:
    normalized = _normalize_text(message)
    week_range = _extract_reference_week_range(message)
    explicit_year = _extract_reference_year(message)
    explicit_month = _extract_reference_month(message)
    explicit_periods = _extract_reference_periods(message)
    explicit_years = _extract_reference_years(message)
    purchase_query = _extract_purchase_query(message)
    catalog_query = _extract_catalog_query(message)
    explicit_homemade_liters = _extract_liters_from_text(message)
    wants_catalog_query_validation = bool(catalog_query and _is_catalog_request(message, normalized, catalog_query))
    wants_latest_batches = _is_latest_batches_request(normalized)
    wants_first_batches = _is_first_batches_request(normalized)
    chronological_order_rank = _extract_chronological_order_rank(message)
    latest_batches_limit = _extract_requested_latest_batches_limit(message)
    if any(tool_call.tool == "get_timeclock_summary" for tool_call in tool_calls):
        normalized_calls: list[PlannedToolCall] = []
        changed = False
        for tool_call in tool_calls:
            if tool_call.tool != "get_timeclock_summary":
                normalized_calls.append(tool_call)
                continue
            normalized_arguments = _normalize_timeclock_tool_arguments(message, normalized, dict(tool_call.arguments))
            normalized_calls.append(PlannedToolCall(tool="get_timeclock_summary", arguments=normalized_arguments))
            if normalized_arguments != tool_call.arguments:
                changed = True
        return normalized_calls if changed else tool_calls
    if _is_homemade_stock_consumption_request(normalized):
        return [_build_homemade_stock_consumption_tool_call(message)]
    if any(
        tool_call.tool == "get_homemade_recipe"
        or (tool_call.tool == "run_tenant_query" and _sql_targets_homemade_stock(str(tool_call.arguments.get("sql") or "")))
        for tool_call in tool_calls
    ) and "consum" in normalized:
        return [_build_homemade_stock_consumption_tool_call(message)]
    if _is_inventory_consumption_estimation_request(normalized) and _has_explicit_inventory_estimation_scope(message, normalized):
        return [_build_inventory_consumption_estimation_tool_call(message)]
    if _is_document_create_request(normalized) and _is_tips_request(message, normalized):
        document_tool_call = next(
            (tool_call for tool_call in tool_calls if tool_call.tool == "create_google_workspace_document"),
            None,
        )
        if document_tool_call is None:
            document_tool_call = _build_document_create_tool_call(message)
        document_arguments = dict(document_tool_call.arguments)
        document_arguments["kind"] = document_arguments.get("kind") or _extract_document_kind(normalized)
        document_arguments["prompt"] = str(document_arguments.get("prompt") or _extract_document_prompt(message)).strip()
        return [
            _build_tips_tool_call(message),
            PlannedToolCall(tool="create_google_workspace_document", arguments=document_arguments),
        ]
    if _is_inventory_request(message, normalized) and _extract_inventory_total_threshold(message) is not None:
        normalized_calls: list[PlannedToolCall] = []
        changed = False
        inventory_threshold_tool_call = _build_inventory_tool_call(message)
        for tool_call in tool_calls:
            if tool_call.tool == "run_tenant_query" and _sql_targets_inventory(str(tool_call.arguments.get("sql") or "")):
                normalized_calls.append(inventory_threshold_tool_call)
                changed = True
                continue
            normalized_calls.append(tool_call)
        if changed:
            return normalized_calls

    if _is_purchase_batch_detail_request(message, normalized):
        batch_id = _extract_purchase_batch_id(message)
        target_date = _extract_purchase_batch_date(message)
        normalized_calls: list[PlannedToolCall] = []
        for tool_call in tool_calls:
            if tool_call.tool != "get_purchase_batches":
                normalized_calls.append(tool_call)
                continue
            arguments = dict(tool_call.arguments)
            arguments["query"] = ""
            arguments["batch_id"] = batch_id
            arguments["target_date"] = None if batch_id is not None else (target_date.isoformat() if isinstance(target_date, date) else None)
            if batch_id is not None or target_date is not None:
                arguments["year"] = None
                arguments["month"] = None
            arguments["limit"] = _CHAT_LIST_LIMIT if target_date is not None and batch_id is None else 1
            normalized_calls.append(PlannedToolCall(tool="get_purchase_batches", arguments=arguments))
        if normalized_calls:
            return normalized_calls

    wants_order_batches = (
        any(keyword in normalized for keyword in ("mostra", "mostrami", "elenca", "elencami", "lista", "vedere", "vedi"))
        and _contains_normalized_word(normalized, "ordine", "ordini")
        and not _is_purchase_product_list_request(normalized)
    )
    wants_purchase_product_list = _is_purchase_product_list_request(normalized)
    wants_order_frequency = (
        any(keyword in normalized for keyword in _ORDERS_KEYWORDS)
        and ("quante volte" in normalized or "quanti ordini" in normalized)
    )
    wants_purchase_amount = _is_purchase_amount_request(normalized)
    wants_purchase_liters = _is_liters_request(normalized)
    wants_purchase_comparison = _is_purchase_comparison_request(message, normalized)
    if _is_document_create_request(normalized) and _document_request_needs_grounded_data(message, normalized):
        return _build_grounded_document_tool_calls(message)

    if _is_fiscal_spend_request(message, normalized):
        return [_build_fiscal_spend_tool_call(message)]

    if _is_supplier_catalog_request(normalized):
        supplier_catalog_tool_call = _build_supplier_catalog_lookup_tool_call(message)
        if supplier_catalog_tool_call is not None:
            return [supplier_catalog_tool_call]

    def _latest_batches_arguments(source_arguments: dict[str, object]) -> dict[str, object]:
        latest_arguments = dict(source_arguments)
        latest_arguments["query"] = purchase_query
        latest_arguments["limit"] = latest_batches_limit
        if week_range is not None:
            latest_arguments["start_date"] = week_range[0].isoformat()
            latest_arguments["end_date"] = week_range[1].isoformat()
            latest_arguments["year"] = explicit_year or week_range[0].year
            latest_arguments["month"] = explicit_month or week_range[0].month
            return latest_arguments

        latest_arguments["start_date"] = None
        latest_arguments["end_date"] = None
        latest_arguments["year"] = explicit_year if explicit_year is not None else None
        latest_arguments["month"] = explicit_month if explicit_month is not None else None
        return latest_arguments

    def _first_batches_arguments(source_arguments: dict[str, object]) -> dict[str, object]:
        first_arguments = dict(source_arguments)
        first_arguments["query"] = purchase_query
        first_arguments["limit"] = chronological_order_rank or 1
        first_arguments["sort_order"] = "earliest"
        if week_range is not None:
            first_arguments["start_date"] = week_range[0].isoformat()
            first_arguments["end_date"] = week_range[1].isoformat()
            first_arguments["year"] = explicit_year or week_range[0].year
            first_arguments["month"] = explicit_month or week_range[0].month
            return first_arguments

        first_arguments["start_date"] = None
        first_arguments["end_date"] = None
        first_arguments["year"] = explicit_year if explicit_year is not None else None
        first_arguments["month"] = explicit_month if explicit_month is not None else None
        return first_arguments

    if wants_first_batches or chronological_order_rank is not None:
        return [PlannedToolCall(tool="get_purchase_batches", arguments=_first_batches_arguments({}))]

    if (
        not wants_order_batches
        and not wants_purchase_product_list
        and not wants_order_frequency
        and not wants_purchase_amount
        and not wants_purchase_liters
        and not wants_purchase_comparison
        and week_range is None
        and not wants_latest_batches
        and not wants_first_batches
        and chronological_order_rank is None
        and explicit_year is None
        and explicit_month is None
        and not wants_catalog_query_validation
    ):
        return tool_calls

    normalized_calls: list[PlannedToolCall] = []
    changed = False
    for tool_call in tool_calls:
        arguments = dict(tool_call.arguments)
        if tool_call.tool == "get_timeclock_summary":
            normalized_arguments = _normalize_timeclock_tool_arguments(message, normalized, arguments)
            normalized_calls.append(PlannedToolCall(tool="get_timeclock_summary", arguments=normalized_arguments))
            if normalized_arguments != tool_call.arguments:
                changed = True
            continue
        if tool_call.tool == "run_tenant_query":
            try:
                requested_limit = int(arguments.get("limit") or _CHAT_LIST_LIMIT)
            except (TypeError, ValueError):
                requested_limit = _CHAT_LIST_LIMIT
            safe_limit = min(max(requested_limit, 1), _CHAT_LIST_LIMIT)
            if arguments.get("limit") != safe_limit:
                arguments["limit"] = safe_limit
                changed = True
        if tool_call.tool in {
            "get_purchase_overview",
            "get_purchase_history",
            "get_purchase_batches",
            "get_purchase_frequency",
        } and (explicit_year is not None or explicit_month is not None):
            if explicit_year is not None:
                arguments["year"] = explicit_year
            if explicit_month is not None:
                arguments["month"] = explicit_month
            if week_range is None:
                arguments["start_date"] = None
                arguments["end_date"] = None
            if arguments != tool_call.arguments:
                changed = True

        if (wants_purchase_amount or wants_purchase_liters) and tool_call.tool in {
            "search_products",
            "get_purchase_history",
            "get_purchase_batches",
            "get_purchase_frequency",
        }:
            normalized_calls.append(
                PlannedToolCall(
                    tool="get_purchase_overview",
                    arguments={
                        "query": str(arguments.get("query") or purchase_query or ""),
                        "year": arguments.get("year") or explicit_year or (week_range[0].year if week_range is not None else None),
                        "month": arguments.get("month") or explicit_month or (week_range[0].month if week_range is not None else None),
                        "start_date": arguments.get("start_date") or (week_range[0].isoformat() if week_range is not None else None),
                        "end_date": arguments.get("end_date") or (week_range[1].isoformat() if week_range is not None else None),
                        "limit": _CHAT_LIST_LIMIT,
                    },
                )
            )
            changed = True
            continue

        if (wants_purchase_amount or wants_purchase_liters) and tool_call.tool == "get_purchase_overview":
            arguments["query"] = str(arguments.get("query") or purchase_query or "")
            arguments["year"] = arguments.get("year") or explicit_year or (week_range[0].year if week_range is not None else None)
            arguments["month"] = arguments.get("month") or explicit_month or (week_range[0].month if week_range is not None else None)
            arguments["start_date"] = arguments.get("start_date") or (week_range[0].isoformat() if week_range is not None else None)
            arguments["end_date"] = arguments.get("end_date") or (week_range[1].isoformat() if week_range is not None else None)
            arguments["limit"] = _CHAT_LIST_LIMIT
            normalized_calls.append(PlannedToolCall(tool="get_purchase_overview", arguments=arguments))
            changed = True
            continue

        if wants_purchase_product_list and tool_call.tool in {
            "search_products",
            "get_purchase_overview",
            "get_purchase_history",
            "get_purchase_batches",
            "get_purchase_frequency",
        }:
            normalized_calls.append(
                PlannedToolCall(
                    tool="get_purchase_overview",
                    arguments={
                        "query": purchase_query,
                        "year": explicit_year or (week_range[0].year if week_range is not None else None),
                        "month": explicit_month or (week_range[0].month if week_range is not None else None),
                        "start_date": week_range[0].isoformat() if week_range is not None else None,
                        "end_date": week_range[1].isoformat() if week_range is not None else None,
                        "limit": _CHAT_LIST_LIMIT,
                    },
                )
            )
            changed = True
            continue
        if wants_latest_batches and not wants_order_batches and tool_call.tool == "get_purchase_batches":
            normalized_calls.append(PlannedToolCall(tool="get_purchase_batches", arguments=_latest_batches_arguments(arguments)))
            changed = True
            continue

        if (wants_first_batches or chronological_order_rank is not None) and tool_call.tool == "get_purchase_batches":
            normalized_calls.append(PlannedToolCall(tool="get_purchase_batches", arguments=_first_batches_arguments(arguments)))
            changed = True
            continue

        if (wants_first_batches or chronological_order_rank is not None) and tool_call.tool in {
            "get_purchase_overview",
            "get_purchase_history",
            "get_purchase_frequency",
        }:
            normalized_calls.append(
                PlannedToolCall(
                    tool="get_purchase_batches",
                    arguments=_first_batches_arguments(arguments),
                )
            )
            changed = True
            continue

        if wants_latest_batches and not wants_order_batches and tool_call.tool in {
            "get_purchase_overview",
            "get_purchase_history",
            "get_purchase_frequency",
        }:
            normalized_calls.append(
                PlannedToolCall(
                    tool="get_purchase_batches",
                    arguments=_latest_batches_arguments(arguments),
                )
            )
            changed = True
            continue

        if week_range is not None and tool_call.tool in {
            "get_purchase_overview",
            "get_purchase_frequency",
            "get_purchase_batches",
            "get_purchase_history",
        }:
            arguments["start_date"] = week_range[0].isoformat()
            arguments["end_date"] = week_range[1].isoformat()
            if arguments.get("year") is None:
                arguments["year"] = explicit_year or week_range[0].year
            if arguments.get("month") is None:
                arguments["month"] = explicit_month or week_range[0].month
            changed = True

        if wants_order_frequency and tool_call.tool in {
            "get_purchase_overview",
            "get_purchase_batches",
            "get_purchase_history",
        }:
            arguments["query"] = str(arguments.get("query") or _extract_purchase_query(message) or "")
            arguments["year"] = arguments.get("year") or explicit_year or (week_range[0].year if week_range is not None else None)
            arguments["month"] = arguments.get("month") or explicit_month or (week_range[0].month if week_range is not None else None)
            arguments["limit"] = _CHAT_LIST_LIMIT
            normalized_calls.append(PlannedToolCall(tool="get_purchase_frequency", arguments=arguments))
            changed = True
            continue

        if wants_order_frequency and tool_call.tool == "get_purchase_frequency":
            arguments["query"] = str(arguments.get("query") or _extract_purchase_query(message) or "")
            arguments["year"] = arguments.get("year") or explicit_year or (week_range[0].year if week_range is not None else None)
            arguments["month"] = arguments.get("month") or explicit_month or (week_range[0].month if week_range is not None else None)
            arguments["limit"] = _CHAT_LIST_LIMIT
            normalized_calls.append(PlannedToolCall(tool="get_purchase_frequency", arguments=arguments))
            changed = True
            continue

        if tool_call.tool == "compare_purchase_periods":
            comparison_query = purchase_query or ""
            arguments["query"] = comparison_query

            if explicit_periods and len(explicit_periods) >= 2:
                fallback_year = explicit_year or explicit_periods[0][0] or explicit_periods[1][0]
                arguments["primary_year"] = explicit_periods[0][0] or fallback_year
                arguments["primary_month"] = explicit_periods[0][1]
                arguments["secondary_year"] = explicit_periods[1][0] or fallback_year
                arguments["secondary_month"] = explicit_periods[1][1]
            elif len(explicit_years) >= 2:
                arguments["primary_year"] = explicit_years[0]
                arguments["secondary_year"] = explicit_years[1]

            arguments["focus_hint"] = _purchase_comparison_focus(normalized, comparison_query)
            arguments["percentage_requested"] = any(
                fragment in normalized for fragment in ("percentuale", "percent", "quanto percent", "%")
            )
            normalized_calls.append(PlannedToolCall(tool="compare_purchase_periods", arguments=arguments))
            changed = True
            continue

        if tool_call.tool == "get_homemade_recipe":
            if explicit_homemade_liters is None and arguments.get("target_liters") is not None:
                arguments.pop("target_liters", None)
                changed = True
            elif explicit_homemade_liters is not None and arguments.get("target_liters") != explicit_homemade_liters:
                arguments["target_liters"] = explicit_homemade_liters
                changed = True

        if wants_order_batches and tool_call.tool == "get_purchase_batches":
            if wants_latest_batches:
                arguments = _latest_batches_arguments(arguments)
            else:
                arguments["query"] = str(arguments.get("query") or "")
                arguments["year"] = arguments.get("year") or explicit_year or (week_range[0].year if week_range is not None else None)
                arguments["month"] = arguments.get("month") or explicit_month or (week_range[0].month if week_range is not None else None)
                arguments["limit"] = _CHAT_LIST_LIMIT
            normalized_calls.append(PlannedToolCall(tool="get_purchase_batches", arguments=arguments))
            changed = True
            continue

        if wants_order_batches and tool_call.tool == "get_purchase_overview":
            normalized_calls.append(
                PlannedToolCall(
                    tool="get_purchase_batches",
                    arguments=_latest_batches_arguments(arguments) if wants_latest_batches else {
                        **arguments,
                        "query": str(arguments.get("query") or ""),
                        "year": arguments.get("year") or explicit_year or (week_range[0].year if week_range is not None else None),
                        "month": arguments.get("month") or explicit_month or (week_range[0].month if week_range is not None else None),
                        "limit": _CHAT_LIST_LIMIT,
                    },
                )
            )
            changed = True
            continue

        if wants_order_batches and tool_call.tool == "get_purchase_history":
            normalized_calls.append(
                PlannedToolCall(
                    tool="get_purchase_batches",
                    arguments=_latest_batches_arguments(arguments) if wants_latest_batches else {
                        **arguments,
                        "query": str(arguments.get("query") or ""),
                        "year": arguments.get("year") or explicit_year or (week_range[0].year if week_range is not None else None),
                        "month": arguments.get("month") or explicit_month or (week_range[0].month if week_range is not None else None),
                        "limit": _CHAT_LIST_LIMIT,
                    },
                )
            )
            changed = True
            continue

        if wants_catalog_query_validation and tool_call.tool == "search_products":
            expected_tokens = _significant_catalog_query_tokens(catalog_query)
            planned_query = str(arguments.get("query") or "").strip()
            if expected_tokens and not _searchable_matches_all_query_tokens(planned_query, expected_tokens):
                arguments["query"] = catalog_query
                changed = True
            if arguments.get("limit") != _CHAT_LIST_LIMIT:
                arguments["limit"] = _CHAT_LIST_LIMIT
                changed = True

        if arguments != tool_call.arguments:
            changed = True
        normalized_calls.append(PlannedToolCall(tool=tool_call.tool, arguments=arguments))
    return normalized_calls if changed else tool_calls


def _build_surface_direct_tool_calls(
    surface: AssistantSurface,
    message: str,
    conversation: list[dict[str, str]] | None = None,
    thread_state: dict[str, object] | None = None,
) -> list[PlannedToolCall]:
    direct_tool_calls = _build_direct_tool_calls(message)
    contextual_tool_calls = _build_contextual_tool_calls(message, conversation or [], thread_state)
    if contextual_tool_calls:
        direct_tool_calls = contextual_tool_calls
    direct_tool_calls = _filter_tool_calls_for_surface(surface, direct_tool_calls)
    if surface == "home":
        return _apply_home_thread_state_to_tool_calls(
            message,
            direct_tool_calls,
            conversation=conversation or [],
            thread_state=thread_state,
        )
    return direct_tool_calls


def _normalize_google_workspace_preview_request(payload: GoogleWorkspacePreviewRequest) -> GoogleWorkspacePreviewRequest:
    effective_title = _extract_document_title(payload.prompt, kind=payload.kind) or payload.title
    effective_prompt = re.sub(
        r"\b(?:nominat[oa]|chiamat[oa]|chiamalo|chiamala|titolo|con\s+nome|nome\s+file)\s+.+$",
        " ",
        payload.prompt,
        flags=re.IGNORECASE,
    )
    effective_prompt = re.sub(r"\s+", " ", effective_prompt).strip(" .,-") or payload.prompt.strip()
    return GoogleWorkspacePreviewRequest(
        kind=payload.kind,
        title=effective_title,
        prompt=effective_prompt,
        destination_folder_id=payload.destination_folder_id,
    )


async def generate_grounded_google_workspace_preview(
    session: SessionIdentity,
    payload: GoogleWorkspacePreviewRequest,
    *,
    surface: AssistantSurface = "documents",
) -> GoogleWorkspaceDocumentPreview | None:
    message = _build_document_request_message_for_tools(payload.prompt, payload.kind)
    normalized = _normalize_text(message)
    if not _document_request_needs_grounded_data(message, normalized):
        return None

    tool_calls = _build_surface_direct_tool_calls(surface, message)
    prior_tool_results: list[dict[str, object]] = []
    for tool_call in tool_calls[:3]:
        if tool_call.tool == "create_google_workspace_document":
            break
        prior_tool_results.append(await _execute_tool_call(session, tool_call, prior_tool_results=prior_tool_results))

    if not prior_tool_results:
        return None

    args = GoogleWorkspaceDocumentArgs(
        kind=payload.kind,
        title=payload.title,
        prompt=payload.prompt,
        destination_folder_id=payload.destination_folder_id,
    )
    return _build_grounded_workspace_preview_from_tool_results(
        args=args,
        prior_tool_results=prior_tool_results,
    )


async def prepare_google_workspace_preview(
    session: SessionIdentity,
    payload: GoogleWorkspacePreviewRequest,
) -> GoogleWorkspaceDocumentPreview:
    outcome = await prepare_google_workspace_preview_with_trace(session, payload)
    return outcome.preview


async def prepare_google_workspace_preview_with_trace(
    session: SessionIdentity,
    payload: GoogleWorkspacePreviewRequest,
) -> GoogleWorkspacePreviewRun:
    normalized_payload = _normalize_google_workspace_preview_request(payload)
    message = _build_document_request_message_for_tools(normalized_payload.prompt, normalized_payload.kind)
    normalized = _normalize_text(message)
    grounded_trace: dict[str, object] = {
        "surface": "documents",
        "kind": normalized_payload.kind,
        "title": normalized_payload.title,
        "prompt": normalized_payload.prompt,
        "normalized_message": normalized,
    }

    direct_purchase_preview = _build_grounded_purchase_document_preview_from_prompt(
        session,
        GoogleWorkspaceDocumentArgs(
            kind=normalized_payload.kind,
            title=normalized_payload.title,
            prompt=normalized_payload.prompt,
            destination_folder_id=normalized_payload.destination_folder_id,
        ),
    )
    if direct_purchase_preview is not None:
        return GoogleWorkspacePreviewRun(
            preview=direct_purchase_preview,
            route="documents-grounded-purchase-preview",
            model="documents-grounded",
            trace=grounded_trace,
        )

    if _document_request_needs_grounded_data(message, normalized):
        tool_calls = _build_surface_direct_tool_calls("documents", message)
        prior_tool_results: list[dict[str, object]] = []
        for tool_call in tool_calls[:3]:
            if tool_call.tool == "create_google_workspace_document":
                break
            prior_tool_results.append(await _execute_tool_call(session, tool_call, prior_tool_results=prior_tool_results))

        if prior_tool_results:
            grounded_preview = _build_grounded_workspace_preview_from_tool_results(
                args=GoogleWorkspaceDocumentArgs(
                    kind=normalized_payload.kind,
                    title=normalized_payload.title,
                    prompt=normalized_payload.prompt,
                    destination_folder_id=normalized_payload.destination_folder_id,
                ),
                prior_tool_results=prior_tool_results,
            )
            if grounded_preview is not None:
                return GoogleWorkspacePreviewRun(
                    preview=grounded_preview,
                    route="documents-grounded-preview",
                    model="documents-grounded",
                    trace={
                        **grounded_trace,
                        "tool_calls": _trace_tool_calls(tool_calls),
                        "tool_results": _trace_tool_results(prior_tool_results),
                    },
                )

    preview = await _generate_preview(normalized_payload, session=session)
    return GoogleWorkspacePreviewRun(
        preview=preview,
        route="documents-llm-preview",
        model="documents-preview-llm",
        trace=grounded_trace,
    )


async def _create_google_workspace_document(
    session: SessionIdentity,
    args: GoogleWorkspaceDocumentArgs,
    *,
    prior_tool_results: list[dict[str, object]] | None = None,
) -> dict[str, object]:
    prompt = _clean_optional_text(args.prompt)
    if not prompt:
        return {
            "status": "clarification_required",
            "detail": "Per creare il documento mi serve almeno il brief o la richiesta da trasformare in file.",
        }

    prior_results = prior_tool_results or []
    request_message = _build_document_request_message_for_tools(prompt, args.kind)
    grounding_required = _document_request_needs_grounded_data(request_message, _normalize_text(request_message))
    prompt_grounded_preview: GoogleWorkspaceDocumentPreview | None = None
    if _document_grounded_sources_are_empty(prior_results):
        return {
            "status": "clarification_required",
            "detail": "Non creo un file vuoto: la lettura dei dati reali del tenant ha restituito 0 righe. Riprova specificando meglio periodo, modulo o filtri.",
        }
    if grounding_required and not prior_results:
        prompt_grounded_preview = _build_grounded_purchase_document_preview_from_prompt(session, args)
        if prompt_grounded_preview is None:
            return {
                "status": "clarification_required",
                "detail": "Non creo file su Drive senza prima aver letto dati reali del tenant. Chiedimi una preview o specifica meglio quali dati devo includere.",
            }

    enriched_prompt = _compose_document_generation_prompt(
        base_prompt=prompt,
        prior_tool_results=prior_results,
    )
    title = _normalize_document_title(args.title, kind=args.kind)

    try:
        connection = await get_active_google_workspace_connection(session)
    except HTTPException as exc:
        return {
            "status": "not_ready",
            "detail": exc.detail if isinstance(exc.detail, str) else "Google Workspace non collegato",
        }

    grounded_preview = _build_grounded_workspace_preview_from_tool_results(
        args=args,
        prior_tool_results=prior_results,
    )
    if grounded_preview is not None:
        preview = grounded_preview
    else:
        if prompt_grounded_preview is None:
            prompt_grounded_preview = _build_grounded_purchase_document_preview_from_prompt(session, args)
        if prompt_grounded_preview is not None:
            preview = prompt_grounded_preview
        elif grounding_required:
            generic_preview = _build_generic_tool_results_document_preview(
                args=args,
                prior_tool_results=prior_results,
            )
            if generic_preview is None:
                return {
                    "status": "clarification_required",
                    "detail": "Non creo file operativi con contenuto generato dall'AI: per questa richiesta non ho un export deterministico basato su dati reali.",
                }
            preview = generic_preview
        else:
            preview = await _generate_preview(
                GoogleWorkspacePreviewRequest(
                    kind=args.kind,
                    title=title,
                    prompt=enriched_prompt,
                    destination_folder_id=args.destination_folder_id,
                ),
                session=session,
            )

    if preview.kind == "doc":
        created = await _create_google_doc(
            GoogleWorkspaceCreateRequest(
                kind="doc",
                title=preview.title,
                destination_folder_id=preview.destination_folder_id,
                content=preview.content or "",
            ),
            connection=connection,
        )
    else:
        created = await _create_google_sheet(
            GoogleWorkspaceCreateRequest(
                kind="sheet",
                title=preview.title,
                destination_folder_id=preview.destination_folder_id,
                columns=preview.columns,
                rows=preview.rows,
            ),
            connection=connection,
        )

    return {
        "status": "created",
        "document": {
            "kind": created.kind,
            "title": created.title,
            "web_url": created.web_url,
            "file_id": created.file_id,
            "destination_folder_id": created.destination_folder_id,
            "account_email": created.account_email,
            "summary": preview.summary,
        },
    }


async def _get_module_settings(session: SessionIdentity, args: GetModuleSettingsArgs) -> dict[str, object]:
    store = get_tenant_store()
    module = args.module
    if module == "ordini":
        with _connect_orders_database(session) as connection:
            rows = list(connection.execute("SELECT module_key, enabled FROM tenant_module_settings ORDER BY module_key ASC"))
        return {
            "module": "ordini",
            "settings": [{"module_key": row["module_key"], "enabled": bool(row["enabled"])} for row in rows],
        }
    if module in ("prenotazioni", "whatsapp"):
        payload = await _prenotazioni_request(session, method="GET", path="/booking-settings")
        if not isinstance(payload, dict):
            return {"module": module, "settings": {}}
        safe = {k: v for k, v in payload.items() if "access_token" not in k}
        return {"module": module, "settings": safe}
    if module == "llm":
        config, updated_at = store.get_llm_settings(session.tenant_id, "home")
        return {"module": "llm", "scope": "home", "config": config, "updated_at": updated_at}
    if module == "fiscal":
        settings = store.get_fiscal_document_settings(session.tenant_id)
        return {"module": "fiscal", "inbound_email": settings.inbound_email, "updated_at": settings.updated_at}
    raise HTTPException(status_code=400, detail=f"Modulo non riconosciuto: {module}")


async def _update_module_settings(session: SessionIdentity, args: UpdateModuleSettingsArgs) -> dict[str, object]:
    store = get_tenant_store()
    module = args.module
    settings = args.settings
    if module in ("prenotazioni", "whatsapp"):
        venue_id = await _get_prenotazioni_venue_id(session)
        payload = await _prenotazioni_request(
            session,
            method="PUT",
            path=f"/booking-settings/{venue_id}",
            json_body=settings,
        )
        if not isinstance(payload, dict):
            return {"module": module, "updated": True}
        safe = {k: v for k, v in payload.items() if "access_token" not in k}
        return {"module": module, "settings": safe}
    if module == "fiscal":
        inbound_email = settings.get("inbound_email")
        result = store.upsert_fiscal_document_settings(
            session.tenant_id,
            inbound_email=str(inbound_email) if inbound_email else None,
        )
        return {"module": "fiscal", "inbound_email": result.inbound_email, "updated_at": result.updated_at}
    if module == "llm":
        updated_at = store.upsert_llm_settings(session.tenant_id, "home", settings)
        return {"module": "llm", "scope": "home", "updated_at": updated_at}
    raise HTTPException(status_code=400, detail=f"Modulo non modificabile tramite questo tool: {module}")


async def _list_reservations_full(session: SessionIdentity, args: ListReservationsArgs) -> dict[str, object]:
    params: dict[str, object] = {}
    if args.date is not None:
        params["reservation_date"] = args.date.isoformat()
    payload = await _prenotazioni_request(session, method="GET", path="/reservations", params=params)
    if isinstance(payload, dict):
        items = payload.get("items", [])
        total = payload.get("total", len(items) if isinstance(items, list) else 0)
    else:
        items = []
        total = 0
    if isinstance(items, list):
        items = items[: args.limit]
    return {"reservations": items, "total": total}


def _list_fiscal_documents_tool(session: SessionIdentity) -> dict[str, object]:
    store = get_tenant_store()
    docs = store.list_fiscal_documents(session.tenant_id)
    return {
        "count": len(docs),
        "documents": [
            {
                "id": doc.id,
                "display_name": doc.display_name,
                "document_type": doc.document_type,
                "document_number": doc.document_number,
                "document_date": doc.document_date,
                "supplier_name": doc.supplier_name,
                "total_amount": doc.total_amount,
                "currency": doc.currency,
                "status": doc.status,
                "review_status": doc.review_status,
                "created_at": doc.created_at,
            }
            for doc in docs
        ],
    }


def _list_tenant_users_tool(session: SessionIdentity) -> dict[str, object]:
    store = get_tenant_store()
    users = store.list_tenant_users(session)
    return {
        "count": len(users),
        "users": [
            {
                "id": u["id"],
                "name": u["name"],
                "username": u["username"],
                "email": u["email"],
                "phone_number": u["phone_number"],
                "role": u["role"],
                "permissions": u["permissions"],
                "created_at": u["created_at"],
            }
            for u in users
        ],
    }


def _resolve_timeclock_date_range(args: TimeclockSummaryArgs) -> tuple[date | None, date | None]:
    if args.scope == "active":
        return None, None
    if args.start_date is not None or args.end_date is not None:
        return args.start_date, args.end_date
    if args.target_date is not None:
        return args.target_date, args.target_date
    today = _today_in_timezone()
    if args.scope == "week":
        return today - timedelta(days=today.weekday()), today
    if args.scope == "all":
        return None, None
    return today, today


def _resolve_timeclock_subject_user(
    session: SessionIdentity,
    query_text: str,
) -> tuple[str | None, dict[str, object] | None]:
    normalized_query = _normalize_text(query_text)
    if session.role not in {"owner", "super_admin"}:
        return session.user_id, {
            "user_id": session.user_id,
            "name": session.user_name,
            "username": session.username,
            "email": session.user_email,
        }
    if any(fragment in f" {normalized_query} " for fragment in (" ho ", " io ", " mie ", " mio ", "miei", "mia")):
        return session.user_id, {
            "user_id": session.user_id,
            "name": session.user_name,
            "username": session.username,
            "email": session.user_email,
        }
    try:
        users = get_tenant_store().list_tenant_users(session)
    except ValueError:
        return None, None

    matches: list[dict[str, object]] = []
    for user in users:
        fields = [
            str(user.get("name") or "").strip(),
            str(user.get("username") or "").strip(),
            str(user.get("email") or "").strip(),
        ]
        normalized_fields = [_normalize_text(field) for field in fields if field]
        if any(field and field in normalized_query for field in normalized_fields):
            matches.append(user)
            continue
        for field in normalized_fields:
            tokens = [token for token in field.split() if len(token) >= 3]
            if tokens and all(token in normalized_query for token in tokens):
                matches.append(user)
                break
    if len(matches) == 1:
        matched = matches[0]
        return str(matched.get("id") or ""), matched
    return None, None


def _get_timeclock_summary_tool(session: SessionIdentity, args: TimeclockSummaryArgs) -> dict[str, object]:
    store = get_tenant_store()
    start_date, end_date = _resolve_timeclock_date_range(args)
    selected_user_id, selected_user = _resolve_timeclock_subject_user(session, args.query_text)
    effective_limit = max(1, min(int(args.limit or _CHAT_LIST_LIMIT), 1000))
    overview = store.get_timeclock_overview(
        session,
        TimeclockOverviewQuery(
            user_id=selected_user_id,
            start_date=start_date.isoformat() if start_date is not None else None,
            end_date=end_date.isoformat() if end_date is not None else None,
            limit=effective_limit,
        ),
    )
    summary_rows = overview.get("summary_by_user") if isinstance(overview.get("summary_by_user"), list) else []
    active_rows = overview.get("active_entries") if isinstance(overview.get("active_entries"), list) else []
    entry_rows = overview.get("entries") if isinstance(overview.get("entries"), list) else []
    target_summary = None
    if selected_user_id and target_summary is None:
        for row in summary_rows:
            if isinstance(row, dict) and str(row.get("user_id") or "") == selected_user_id:
                target_summary = row
                break

    return {
        "scope": args.scope,
        "target_date": args.target_date.isoformat() if args.target_date is not None else None,
        "start_date": start_date.isoformat() if start_date is not None else None,
        "end_date": end_date.isoformat() if end_date is not None else None,
        "resolved_user": {
            "user_id": selected_user_id,
            "name": None if selected_user is None else selected_user.get("name"),
            "username": None if selected_user is None else selected_user.get("username"),
            "email": None if selected_user is None else selected_user.get("email"),
        },
        "selected_summary": target_summary,
        "summary_by_user": summary_rows,
        "active_entries": active_rows,
        "entries": entry_rows,
        "include_entries": args.include_entries,
    }


def _resolve_inventory_consumption_dates(
    available_dates: list[str],
    *,
    start_date: date | None,
    end_date: date | None,
) -> tuple[str | None, str | None]:
    normalized_dates: list[date] = []
    for value in available_dates:
        try:
            normalized_dates.append(date.fromisoformat(str(value)))
        except ValueError:
            continue
    if not normalized_dates:
        return None, None

    normalized_dates = sorted(set(normalized_dates), reverse=True)
    if end_date is not None:
        end_candidates = [item for item in normalized_dates if item <= end_date]
        selected_end = end_candidates[0] if end_candidates else None
    else:
        selected_end = normalized_dates[0]
    if selected_end is None:
        return None, None

    older_candidates = [item for item in normalized_dates if item < selected_end]
    if start_date is not None:
        start_candidates = [item for item in older_candidates if item <= start_date]
        selected_start = start_candidates[0] if start_candidates else None
    else:
        selected_start = older_candidates[0] if older_candidates else None

    return selected_start.isoformat() if selected_start is not None else None, selected_end.isoformat()


def _get_inventory_consumption_tool(session: SessionIdentity, args: InventoryConsumptionArgs) -> dict[str, object]:
    store = get_tenant_store()
    history_payload = store.list_inventory_totals_history(session)
    history_entries = history_payload.get("entries") if isinstance(history_payload, dict) else []
    available_dates = [
        str(entry.get("inventory_date") or "").strip()
        for entry in history_entries
        if isinstance(entry, dict) and str(entry.get("inventory_date") or "").strip()
    ]
    start_inventory_date, end_inventory_date = _resolve_inventory_consumption_dates(
        available_dates,
        start_date=args.start_date,
        end_date=args.end_date,
    )
    if not start_inventory_date or not end_inventory_date:
        return {
            "query": args.query,
            "items": [],
            "start_inventory_date": start_inventory_date,
            "end_inventory_date": end_inventory_date,
            "total_consumed_units": 0.0,
            "period_days": 0,
            "reason": "insufficient_history",
        }

    opening_detail = store.get_inventory_totals_detail(session, inventory_date=start_inventory_date)
    closing_detail = store.get_inventory_totals_detail(session, inventory_date=end_inventory_date)
    opening_items = opening_detail.get("items") if isinstance(opening_detail.get("items"), list) else []
    closing_items = closing_detail.get("items") if isinstance(closing_detail.get("items"), list) else []

    opening_map = {
        (_normalize_lookup(str(item.get("product_name") or "")), _normalize_lookup(str(item.get("supplier_name") or ""))): item
        for item in opening_items
        if isinstance(item, dict)
    }
    closing_map = {
        (_normalize_lookup(str(item.get("product_name") or "")), _normalize_lookup(str(item.get("supplier_name") or ""))): item
        for item in closing_items
        if isinstance(item, dict)
    }

    matched_rows: list[tuple[float, dict[str, object]]] = []
    for key in set(opening_map) | set(closing_map):
        opening_item = opening_map.get(key) if isinstance(opening_map.get(key), dict) else {}
        closing_item = closing_map.get(key) if isinstance(closing_map.get(key), dict) else {}
        product_name = str((opening_item or closing_item).get("product_name") or "")
        supplier_name = str((opening_item or closing_item).get("supplier_name") or "")
        searchable = " ".join(part for part in (product_name, supplier_name) if part).strip()
        score = _score_product_match(args.query, searchable) if args.query.strip() else 1.0
        if args.query.strip() and score <= 0:
            continue
        opening_units = float((opening_item or {}).get("total_equivalent_units") or 0)
        closing_units = float((closing_item or {}).get("total_equivalent_units") or 0)
        consumed_units = round(opening_units - closing_units, 3)
        if consumed_units <= 0:
            continue
        matched_rows.append(
            (
                score,
                {
                    "product_name": product_name,
                    "supplier_name": supplier_name,
                    "opening_units": round(opening_units, 3),
                    "closing_units": round(closing_units, 3),
                    "consumed_units": consumed_units,
                },
            )
        )

    matched_rows.sort(
        key=lambda entry: (
            -entry[0],
            -float(entry[1]["consumed_units"]),
            str(entry[1]["supplier_name"]).lower(),
            str(entry[1]["product_name"]).lower(),
        )
    )
    selected_items = [row for _, row in matched_rows[: args.limit]]

    try:
        period_days = max((date.fromisoformat(end_inventory_date) - date.fromisoformat(start_inventory_date)).days, 0)
    except ValueError:
        period_days = 0

    return {
        "query": args.query,
        "items": selected_items,
        "start_inventory_date": start_inventory_date,
        "end_inventory_date": end_inventory_date,
        "total_consumed_units": round(sum(float(item["consumed_units"]) for item in selected_items), 3),
        "period_days": period_days,
        "available_history_dates": available_dates,
    }


def _manage_tenant_user(session: SessionIdentity, args: ManageTenantUserArgs) -> dict[str, object]:
    store = get_tenant_store()
    if args.operation == "delete":
        if not args.user_id:
            raise HTTPException(status_code=400, detail="user_id obbligatorio per eliminare un sotto-account.")
        try:
            store.delete_tenant_staff_user(session, args.user_id)
        except (ValueError, KeyError) as exc:
            raise HTTPException(status_code=400, detail=str(exc)) from exc
        return {"operation": "delete", "user_id": args.user_id, "deleted": True}
    if args.operation == "create":
        name = (args.name or "").strip()
        username = (args.username or "").strip()
        email = (args.email or "").strip()
        password = (args.password or "").strip()
        if not name or not username or not email or not password:
            raise HTTPException(
                status_code=400,
                detail="Per creare un sotto-account servono: name, username, email, password.",
            )
        try:
            payload = TenantStaffUserCreatePayload(
                name=name,
                username=username,
                email=email,
                phone_number=args.phone_number,
                password=password,
                permissions=args.permissions,
            )
            user = store.create_tenant_staff_user(session, payload)
        except (ValueError, KeyError) as exc:
            raise HTTPException(status_code=400, detail=str(exc)) from exc
        return {"operation": "create", "user": user}
    raise HTTPException(status_code=400, detail=f"Operazione non supportata: {args.operation}")


def _update_venue_profile(session: SessionIdentity, args: UpdateVenueProfileArgs) -> dict[str, object]:
    if not any([args.venue_name, args.address, args.phone_number, args.whatsapp_number]):
        raise HTTPException(status_code=400, detail="Specifica almeno un campo da aggiornare.")
    try:
        get_tenant_store().update_venue_info(
            session,
            venue_name=args.venue_name,
            address=args.address,
            phone_number=args.phone_number,
            whatsapp_number=args.whatsapp_number,
        )
    except (ValueError, KeyError) as exc:
        raise HTTPException(status_code=400, detail=str(exc)) from exc
    return _get_locale_profile(session)


async def _execute_tool_call(
    session: SessionIdentity,
    tool_call: PlannedToolCall,
    *,
    prior_tool_results: list[dict[str, object]] | None = None,
) -> dict[str, object]:
    if tool_call.tool == "get_locale_profile":
        result = _get_locale_profile(session)
    elif tool_call.tool == "search_products":
        result = _search_products(session, SearchProductsArgs.model_validate(tool_call.arguments))
    elif tool_call.tool == "upsert_product":
        result = _upsert_product(session, ProductWriteArgs.model_validate(tool_call.arguments))
    elif tool_call.tool == "get_purchase_overview":
        result = _purchase_overview(session, PurchaseOverviewArgs.model_validate(tool_call.arguments))
    elif tool_call.tool == "compare_purchase_periods":
        result = _purchase_period_comparison(session, PurchaseComparisonArgs.model_validate(tool_call.arguments))
    elif tool_call.tool == "get_purchase_frequency":
        result = _purchase_frequency(session, PurchaseFrequencyArgs.model_validate(tool_call.arguments))
    elif tool_call.tool == "get_purchase_batches":
        result = _purchase_batches(session, PurchaseBatchesArgs.model_validate(tool_call.arguments))
    elif tool_call.tool == "get_purchase_history":
        result = _purchase_history(session, PurchaseHistoryArgs.model_validate(tool_call.arguments))
    elif tool_call.tool == "get_suspended_order":
        result = _read_suspended_order(session, SuspendedOrderReadArgs.model_validate(tool_call.arguments))
    elif tool_call.tool == "write_suspended_order":
        result = _write_suspended_order(session, SuspendedOrderWriteArgs.model_validate(tool_call.arguments))
    elif tool_call.tool == "get_sales_goals":
        result = _sales_goals(session, SalesGoalsArgs.model_validate(tool_call.arguments))
    elif tool_call.tool == "write_sales_goal":
        result = _write_sales_goal(session, SalesGoalWriteArgs.model_validate(tool_call.arguments))
    elif tool_call.tool == "list_shared_notes":
        result = _shared_notes(session, SharedNotesArgs.model_validate(tool_call.arguments))
    elif tool_call.tool == "write_shared_note":
        result = _write_shared_note(session, SharedNoteWriteArgs.model_validate(tool_call.arguments))
    elif tool_call.tool == "get_reservations_snapshot":
        result = await _reservations_snapshot(session, ReservationsSnapshotArgs.model_validate(tool_call.arguments))
    elif tool_call.tool == "create_reservation":
        result = await _create_reservation(session, ReservationCreateArgs.model_validate(tool_call.arguments))
    elif tool_call.tool == "update_reservation":
        result = await _update_reservation(session, ReservationUpdateArgs.model_validate(tool_call.arguments))
    elif tool_call.tool == "delete_reservation":
        result = await _delete_reservation(session, ReservationDeleteArgs.model_validate(tool_call.arguments))
    elif tool_call.tool == "create_google_workspace_document":
        result = await _create_google_workspace_document(
            session,
            GoogleWorkspaceDocumentArgs.model_validate(tool_call.arguments),
            prior_tool_results=prior_tool_results,
        )
    elif tool_call.tool == "get_module_settings":
        result = await _get_module_settings(session, GetModuleSettingsArgs.model_validate(tool_call.arguments))
    elif tool_call.tool == "update_module_settings":
        result = await _update_module_settings(session, UpdateModuleSettingsArgs.model_validate(tool_call.arguments))
    elif tool_call.tool == "list_reservations":
        result = await _list_reservations_full(session, ListReservationsArgs.model_validate(tool_call.arguments))
    elif tool_call.tool == "list_fiscal_documents":
        result = _list_fiscal_documents_tool(session)
    elif tool_call.tool == "list_tenant_users":
        result = _list_tenant_users_tool(session)
    elif tool_call.tool == "get_timeclock_summary":
        result = _get_timeclock_summary_tool(session, TimeclockSummaryArgs.model_validate(tool_call.arguments))
    elif tool_call.tool == "get_inventory_consumption":
        result = _get_inventory_consumption_tool(session, InventoryConsumptionArgs.model_validate(tool_call.arguments))
    elif tool_call.tool == "get_homemade_recipe":
        result = _get_homemade_recipe_tool(session, HomemadeRecipeArgs.model_validate(tool_call.arguments))
    elif tool_call.tool == "manage_tenant_user":
        result = _manage_tenant_user(session, ManageTenantUserArgs.model_validate(tool_call.arguments))
    elif tool_call.tool == "update_venue_profile":
        result = _update_venue_profile(session, UpdateVenueProfileArgs.model_validate(tool_call.arguments))
    elif tool_call.tool == "describe_tenant_schema":
        result = await _describe_tenant_schema_tool(session, DescribeTenantSchemaArgs.model_validate(tool_call.arguments))
    elif tool_call.tool == "run_tenant_query":
        result = await _run_tenant_query_tool(session, RunTenantQueryArgs.model_validate(tool_call.arguments))
    else:
        raise HTTPException(status_code=400, detail=f"Tool non supportato: {tool_call.tool}")

    return {
        "tool": tool_call.tool,
        "arguments": tool_call.arguments,
        "result": result,
    }


def _clean_model_name(value: str | None) -> str | None:
    cleaned = (value or "").strip()
    return cleaned or None


def _assistant_planner_model() -> str | None:
    settings = get_settings()
    return _clean_model_name(settings.assistant_planner_model) or _clean_model_name(settings.assistant_model)


def _assistant_synthesis_model() -> str | None:
    settings = get_settings()
    return _clean_model_name(settings.assistant_synthesis_model) or _clean_model_name(settings.assistant_model)


def _planner_tool_catalog_summary() -> str:
    return "; ".join(
        [
            "search_products(query,limit): catalogo prodotti attivi",
            "get_purchase_overview(query,year,month,start_date,end_date,limit): totali acquistati",
            "get_purchase_history(query,year,month,start_date,end_date,limit): righe acquisto",
            "get_purchase_batches(query,year,month,start_date,end_date,batch_id,target_date,limit): ordini/batch",
            "compare_purchase_periods(query,primary_year,primary_month,secondary_year,secondary_month,limit): confronti",
            "get_purchase_frequency(query,year,month,start_date,end_date,limit): frequenza ordini",
            "run_tenant_query(sql,limit): SELECT SQLite read-only",
            "describe_tenant_schema(include_examples): schema disponibile",
            "list_tenant_users(): utenti/account",
            "get_reservations_snapshot(target_date,target_time,time_window,customer_query,limit): stato prenotazioni",
            "list_reservations(date,limit): elenco prenotazioni",
            "create_reservation/update_reservation/delete_reservation: scrittura prenotazioni",
            "get_inventory_consumption(query,start_date,end_date,limit): consumi tra inventari",
            "get_homemade_recipe(query,target_liters): ricette prep",
            "get_timeclock_summary(query_text,scope,target_date,start_date,end_date,limit,include_entries): turni/cartellini; scope solo today|week|all|active, per mese/anno usa start_date/end_date",
            "get_module_settings/update_module_settings: configurazioni moduli",
            "create_google_workspace_document(kind,title,prompt,destination_folder_id): crea doc/sheet",
            "upsert_product/manage_tenant_user/update_venue_profile/write_shared_note/write_sales_goal/write_suspended_order: scritture controllate",
        ]
    )


def _planner_schema_hints() -> str:
    return "; ".join(
        [
            "ordini_products: product_name, lot_code, supplier_name, product_code, final_price_vat, units_per_pack, liters_per_unit, active",
            "ordini_items + ordini_batches: storico acquisti, confirmed_at, quantity",
            "supplier_catalog_items: source_name, source_lot_code, source_supplier_name, product_code, final_price_vat, units_per_pack, liters_per_unit, catalog_id",
            "supplier_catalogs: id, supplier_name, catalog_name, total_items",
            "inventory_latest_items: warehouse_name, product_name, supplier_name, total_equivalent_units, inventory_source, inventory_date",
            "tenant_inventory_daily_consumptions: consumi salvati per magazzino/prodotto/data, consumed_units, period_days",
            "tenant_inventory_estimated_consumptions: consumi stimati globali per prodotto/data; formula deterministicamente opening_units + incoming_units - closing_units = consumed_units",
            "tenant_inventory_consumption_product_stats: medie consumi stimate per prodotto; average_daily_consumed_units = totale consumato stimato / giorni con almeno un consumo stimato",
            "tenant_homemade_stock_movements: movimenti stock prep/homemade; consumed_quantity > 0 = prep consumata/scaricata, added_quantity > 0 = prep prodotta/caricata, occurred_at = data movimento",
            "tenant_homemade_stock_items: stock corrente prep/homemade per frigo/magazzino, quantity in pz",
            "tenant_homemade_operational_days: giorni operativi derivati dai calendari stock homemade, usage_scope bar/restaurant e operational_date",
            "tenant_homemade_recipes: ricette/prep homemade, name, measurement_unit, usage_scope",
            "tenant_users: name, username, email, role, permissions",
            "tenant_timeclock_entries: user_name, username, started_at, ended_at, duration_seconds",
            "tenant_tips_runs: id, area, tip_date, total_tip_amount, tip_pos_amount, tip_pos_effective_amount, tip_cash_amount, historical_total_amount, payable_total_amount, present_staff_count, payout_status. total_tip_amount e' il lordo raccolto; payable_total_amount e' il totale realmente da pagare, quindi usa il POS effettivo netto. payout_status indica pending/da consegnare, carried/caricata in altra giornata o settled/consegnata.",
            "tenant_tips_run_entries: run_id, area, staff_name, staff_lookup, amount_today, historical_amount, total_amount, score, present",
        ]
    )


def _planner_system_prompt(session: SessionIdentity) -> str:
    return "\n".join(
        [
            "Sei il planner operativo di PowerUp. Non sei user-facing: devi decidere se rispondere o quali tool reali usare.",
            "Output obbligatorio: SOLO JSON valido. Formato reply: {\"mode\":\"reply\",\"reply\":\"...\",\"tool_calls\":[],\"confidence\":0.0-1.0,\"needs_clarification\":false}. Formato tool: {\"mode\":\"tool\",\"reply\":null,\"tool_calls\":[{\"tool\":\"nome_tool\",\"arguments\":{...}}],\"confidence\":0.0-1.0,\"needs_clarification\":false}.",
            "Usa i tool per ogni richiesta su dati o azioni del locale: prodotti, cataloghi, ordini, inventario, consumi, documenti fiscali, prenotazioni, turni, mance, homemade, note, utenti, impostazioni o file Google.",
            "Non inventare dati. Se mancano dettagli indispensabili, usa mode=reply e fai una sola domanda chiara.",
            "Il Contesto runtime nel messaggio utente contiene data/ora e locale: usalo per oggi, domani, dopodomani, questa settimana, settimana prossima, questo mese, mese prossimo, anno corrente e simili. Converti sempre in YYYY-MM-DD quando passi argomenti ai tool.",
            "Follow-up: usa stato thread e conversazione recente per capire riferimenti brevi tipo 'e questo?', 'me li mostri tutti?', 'quanto costa?', ma riconosci cambi espliciti di dominio. Non restare su timbrature se la nuova domanda parla di inventario, catalogo o ordini.",
            "Preferisci describe_tenant_schema + run_tenant_query per aggregazioni libere, join, classifiche, conteggi, totali, percentuali e domande non coperte perfettamente dai tool specializzati. Se usi run_tenant_query e non sei certo dello schema, includi anche describe_tenant_schema nello stesso piano.",
            "SQLite: solo SELECT read-only; niente ILIKE, usa lower(colonna) LIKE '%termine%'. Scegli LIMIT piccoli: 1 per totali singoli, N per top/lista richiesta, 50 se non specificato. Non usare 5000 salvo richiesta di lista completa.",
            "Timeclock/turni/ore: usa get_timeclock_summary o SQL sulle tabelle timeclock con start_date/end_date per mese, settimana, anno o periodi relativi.",
            "Mance: usa SQL su tenant_tips_runs, tenant_tips_run_entries e tenant_tips_roster_entries; per report/storico non filtrare staff salvo persona specifica. Per totali da pagare usa payable_total_amount, non il lordo POS. Per mance ancora da consegnare filtra tenant_tips_runs.payout_status = 'pending'.",
            "Inventario/giacenze/magazzini: usa SQL su inventory_warehouses, inventory_latest_items, inventory_latest_lots. Per 'in casa' o senza magazzino aggrega su tutti i magazzini; per soglie globali usa GROUP BY product_name/supplier_name e HAVING SUM(total_equivalent_units) < soglia.",
            "Classifiche inventario: aggrega per prodotto/fornitore e ordina per SUM(total_equivalent_units) DESC o ASC secondo richiesta.",
            "Cataloghi fornitori: per 'chi vende', 'catalogo Laconi', prezzi fornitore o confronti fornitori usa supplier_catalogs/supplier_catalog_items, non search_products. search_products serve al catalogo prodotti del locale.",
            "Ordini/acquisti/storico: usa i tool ordini o SQL su ordini_batches/ordini_items/ordini_products. Per richieste senza prodotto/fornitore esplicito usa query vuota, non parole generiche come 'finora', 'mese', 'ordine'.",
            "Confronti ordini: se l'utente dice 'questo mese contro maggio 2025', interpreta 'questo mese' come mese corrente del Contesto runtime e 'maggio 2025' come secondo periodo. Se non nomina un prodotto o fornitore, passa query=''. Non inserire parole come allora/questo/mese/contro/confronta nella query.",
            "Documenti fiscali: usa tenant_fiscal_documents e tenant_fiscal_document_items per bolle, fatture, DDT, merce arrivata, discrepanze o valori documento.",
            "Spese/pagato/importi reali: per 'quanto abbiamo speso/pagato' usa come fonte primaria tenant_fiscal_documents.total_amount, che rappresenta il totale documento IVA inclusa. Usa ordini solo per quantita ordinate o stime quando non esiste documento fiscale.",
            "Nei documenti fiscali supplier_name e' il fornitore normalizzato. Non prendere come fornitore il cliente/fatturazione presente nel testo OCR, per esempio Sammy Productions e' il destinatario del locale, non il fornitore MOET.",
            "Consumi/stime: get_inventory_consumption solo per cali tra inventari. Per stime consumo medio usa dati reali da ordini + inventari/giacenze e dichiara la formula e gli eventuali limiti.",
            "Consumi prep/homemade/preparazioni: usa SQL su tenant_homemade_stock_movements, tenant_homemade_stock_items e tenant_homemade_operational_days. La media giornaliera va divisa per i giorni operativi calendario dell'area della prep: usage_scope restaurant usa il calendario ristorante, bar usa bar/club, both usa l'unione. Non usare get_inventory_consumption ne tenant_inventory_consumption_product_stats per prep/homemade.",
            "Homemade/prep/ricette: usa get_homemade_recipe solo per formule, ingredienti, dosi e rese delle ricette.",
            "File Google: se il file deve contenere dati reali, prima raccogli i dati con i tool corretti, poi create_google_workspace_document come ultimo step. Non creare file con dati finti.",
            "Scritture: per prenotazioni, prodotti, obiettivi, note, ordini sospesi, utenti o impostazioni usa solo i tool dedicati; mai dire che hai scritto/modificato se non hai chiamato il tool.",
            f"Tool disponibili: {_planner_tool_catalog_summary()}",
            f"Schema rapido: {_planner_schema_hints()}",
        ]
    )

    now = _now_in_timezone()
    tool_descriptions = [
        {
            "tool": "get_locale_profile",
            "when_to_use": "Profilo locale, contatti, moduli attivi, dati base del tenant.",
            "arguments": {},
        },
        {
            "tool": "describe_tenant_schema",
            "when_to_use": "Leggere lo schema SQL read-only del tenant prima di costruire query libere su prodotti, cataloghi fornitori, ordini, utenti, prenotazioni, documenti fiscali o impostazioni runtime.",
            "arguments": {"include_examples": True},
        },
        {
            "tool": "run_tenant_query",
            "when_to_use": "Eseguire query SQL SELECT read-only sul data mart sicuro del tenant per report, join, aggregazioni, conteggi, percentuali e confronti non coperti bene dai tool specifici, inclusi i cataloghi fornitori separati dal catalogo prodotti locale.",
            "arguments": {"sql": "SELECT supplier_name, COUNT(*) AS total FROM ordini_products GROUP BY supplier_name ORDER BY total DESC, supplier_name ASC LIMIT 10", "limit": _CHAT_LIST_LIMIT},
        },
        {
            "tool": "search_products",
            "when_to_use": "Catalogo prodotti attivi del locale, marche, fornitori, formati.",
            "arguments": {"query": "stringa libera", "limit": _CHAT_LIST_LIMIT},
        },
        {
            "tool": "upsert_product",
            "when_to_use": "Creare o aggiornare un prodotto del catalogo del locale raccogliendo quanti piu dati affidabili possibile. Il nome prodotto e il minimo indispensabile; lotto e fornitore possono essere completati dopo.",
            "arguments": {
                "product_name": "Bombay Sapphire 70cl",
                "lot_code": "bt",
                "supplier_name": "FERRO",
                "final_price_vat": 13.26,
            },
        },
        {
            "tool": "get_purchase_overview",
            "when_to_use": "Capire cosa compra il locale, quali marche/fornitori usa, quanto ordina un prodotto.",
            "arguments": {"query": "stringa libera", "year": now.year, "limit": _CHAT_LIST_LIMIT},
        },
        {
            "tool": "compare_purchase_periods",
            "when_to_use": "Confrontare storico ordini tra due anni oppure tra due mesi/periodi specifici per prodotto, marca o categoria.",
            "arguments": {"query": "bombay", "primary_year": now.year, "primary_month": 8, "secondary_year": now.year - 1, "secondary_month": 8, "limit": _CHAT_LIST_LIMIT},
        },
        {
            "tool": "get_purchase_frequency",
            "when_to_use": "Capire quante volte il locale ha ordinato un prodotto o una marca in un periodo.",
            "arguments": {"query": "bombay", "year": now.year, "limit": _CHAT_LIST_LIMIT},
        },
        {
            "tool": "get_purchase_batches",
            "when_to_use": "Mostrare ordini completi/batch reali del locale, per esempio ultimo ordine o ultimi ordini di un anno.",
            "arguments": {"query": "stringa libera opzionale", "year": now.year, "limit": _CHAT_LIST_LIMIT},
        },
        {
            "tool": "get_purchase_history",
            "when_to_use": "Vedere righe storiche dettagliate di acquisto per prodotti o fornitori, anche filtrando per mese.",
            "arguments": {"query": "stringa libera", "year": now.year, "month": now.month, "limit": _CHAT_LIST_LIMIT},
        },
        {
            "tool": "get_suspended_order",
            "when_to_use": "Leggere l'ordine sospeso attuale.",
            "arguments": {"staff": "opzionale"},
        },
        {
            "tool": "write_suspended_order",
            "when_to_use": "Creare o aggiornare l'ordine sospeso con prodotti e quantita.",
            "arguments": {
                "operation": "set o add",
                "items": [{"product_query": "bombay 70 cl", "quantity": 1}],
                "staff": "opzionale",
            },
        },
        {
            "tool": "get_sales_goals",
            "when_to_use": "Obiettivi vendita o target caricati nel modulo ordini.",
            "arguments": {"year": 2026},
        },
        {
            "tool": "write_sales_goal",
            "when_to_use": "Creare, aggiornare o eliminare un obiettivo vendita/target reale del locale.",
            "arguments": {
                "operation": "upsert | delete",
                "year": 2026,
                "name": "Bombay estate",
                "goal_type": "quantity | liters | liters_dual | note",
                "product_match": "bombay",
                "target": 24,
            },
        },
        {
            "tool": "list_shared_notes",
            "when_to_use": "Note condivise del locale.",
            "arguments": {"limit": _CHAT_LIST_LIMIT},
        },
        {
            "tool": "write_shared_note",
            "when_to_use": "Creare, aggiornare o eliminare una nota condivisa del locale.",
            "arguments": {"operation": "create | update | delete", "text": "stringa nota", "match_text": "testo per trovare la nota"},
        },
        {
            "tool": "get_reservations_snapshot",
            "when_to_use": "Prenotazioni del locale, sale, capienza tavoli, posti occupati/liberi e stato operativo per un giorno o un orario preciso.",
            "arguments": {
                "target_date": now.date().isoformat(),
                "target_time": "20:30 opzionale",
                "time_window": "all_day | lunch | evening",
                "customer_query": "opzionale",
                "limit": _CHAT_LIST_LIMIT,
            },
        },
        {
            "tool": "create_reservation",
            "when_to_use": "Creare una prenotazione dal gestionale centrale.",
            "arguments": {
                "customer_name": "Mario Rossi",
                "customer_phone": "+39 333 1234567",
                "reservation_date": now.date().isoformat(),
                "start_time": "20:30",
                "guests": 4,
            },
        },
        {
            "tool": "update_reservation",
            "when_to_use": "Modificare una prenotazione esistente usando id oppure nome/data/orario/telefono.",
            "arguments": {
                "customer_query": "Mario Rossi",
                "target_date": now.date().isoformat(),
                "target_time": "20:30",
                "new_start_time": "21:00",
                "new_guests": 5,
            },
        },
        {
            "tool": "delete_reservation",
            "when_to_use": "Cancellare una prenotazione esistente usando id oppure nome/data/orario/telefono.",
            "arguments": {
                "customer_query": "Mario Rossi",
                "target_date": now.date().isoformat(),
                "target_time": "20:30",
            },
        },
        {
            "tool": "create_google_workspace_document",
            "when_to_use": "Creare un Google Doc o Google Sheet dal brief del gestore; se il documento deve essere basato su dati reali, usa prima i tool dati e poi questo come ultimo step.",
            "arguments": {
                "kind": "doc | sheet",
                "title": f"Report ordini {now.year}",
                "prompt": "Brief del documento o del foglio",
                "destination_folder_id": "opzionale",
            },
        },
        {
            "tool": "get_module_settings",
            "when_to_use": "Leggere la configurazione di un modulo del locale: ordini, prenotazioni/whatsapp, documenti fiscali (fiscal) o LLM.",
            "arguments": {"module": "ordini | prenotazioni | whatsapp | fiscal | llm"},
        },
        {
            "tool": "update_module_settings",
            "when_to_use": "Aggiornare la configurazione di un modulo del locale (prenotazioni, whatsapp, fiscal, llm).",
            "arguments": {
                "module": "prenotazioni | whatsapp | fiscal | llm",
                "settings": {"chiave": "valore"},
            },
        },
        {
            "tool": "list_reservations",
            "when_to_use": "Elenco dettagliato delle prenotazioni del locale, opzionalmente filtrate per data.",
            "arguments": {"date": now.date().isoformat(), "limit": _CHAT_LIST_LIMIT},
        },
        {
            "tool": "list_fiscal_documents",
            "when_to_use": "Elenco di tutti i documenti fiscali (fatture, DDT, ecc.) caricati nel locale.",
            "arguments": {},
        },
        {
            "tool": "list_tenant_users",
            "when_to_use": "Elenco degli utenti/staff del locale con ruoli e permessi.",
            "arguments": {},
        },
        {
            "tool": "get_timeclock_summary",
            "when_to_use": "Leggere cartellini, ore lavorate, turni attivi e riepiloghi staff del locale.",
            "arguments": {
                "query_text": "quante ore ha fatto Mario oggi",
                "scope": "today | week | all | active",
                "target_date": now.date().isoformat(),
                "start_date": "opzionale per settimane/mesi/anni",
                "end_date": "opzionale per settimane/mesi/anni",
                "limit": _CHAT_LIST_LIMIT,
                "include_entries": False,
            },
        },
        {
            "tool": "get_inventory_consumption",
            "when_to_use": "Stimare il calo di giacenza tra due inventari totali del locale per prodotto, marca o categoria, usando lo storico inventario aggregato dei magazzini.",
            "arguments": {
                "query": "vodka",
                "start_date": None,
                "end_date": None,
                "limit": _CHAT_LIST_LIMIT,
            },
        },
        {
            "tool": "get_homemade_recipe",
            "when_to_use": "Leggere ricette homemade/prep interne del locale, cercare quali ricette contengono un ingrediente e calcolare le dosi per un certo numero di litri, anche espandendo prep annidate.",
            "arguments": {
                "query": "sweet sour",
                "target_liters": 5,
            },
        },
        {
            "tool": "manage_tenant_user",
            "when_to_use": "Creare o eliminare un sotto-account staff del locale.",
            "arguments": {
                "operation": "create | delete",
                "name": "Mario Rossi",
                "username": "mario.rossi",
                "email": "mario@locale.it",
                "password": "password_sicura",
                "permissions": [],
            },
        },
        {
            "tool": "update_venue_profile",
            "when_to_use": "Aggiornare nome, indirizzo, telefono o numero WhatsApp del locale.",
            "arguments": {
                "venue_name": "opzionale",
                "address": "opzionale",
                "phone_number": "opzionale",
                "whatsapp_number": "opzionale",
            },
        },
    ]

    return "\n".join(
        [
            "Sei un motore di orchestrazione per l'assistente operativo del locale.",
            "Non sei user-facing. Devi decidere se rispondere direttamente o usare tool reali.",
            "Sei il decisore semantico principale: interpreta intenzione, contesto e follow-up prima di scegliere i tool. Non ragionare come un parser di keyword.",
            "Non usare mai il tool-calling nativo dell'API LLM: devi scrivere soltanto JSON testuale nel content della risposta.",
            "Usa SEMPRE i tool se l'utente chiede dati del locale, storico acquisti, prodotti, fornitori, prenotazioni, note, obiettivi o azioni sugli ordini sospesi.",
            "Per letture complesse, aggregazioni SQL, join tra tabelle o domande libere sui dati del tenant, usa describe_tenant_schema e poi run_tenant_query.",
            "Non restituire mai un piano con solo describe_tenant_schema: se hai bisogno dello schema, includi anche run_tenant_query nello stesso piano.",
            "Le query di run_tenant_query usano SQLite: non usare ILIKE, usa lower(colonna) LIKE '%termine%' oppure LIKE.",
            "Per classifiche, top N, conteggi per gruppo o domande del tipo 'quali sono i primi fornitori/prodotti/utenti/documenti ...', preferisci describe_tenant_schema + run_tenant_query invece dei tool storici.",
            "run_tenant_query e' SOLO read-only: usalo per leggere e analizzare dati, mai per scrivere o simulare scritture.",
            "Per cartellini, turni, timbrature, ore lavorate o presenze staff usa describe_tenant_schema + run_tenant_query sulle tabelle timeclock del tenant.",
            "Per mance, divisioni mance, storico mance o domande del tipo 'quante mance ha preso X' usa describe_tenant_schema + run_tenant_query sulle tabelle tenant_tips_runs, tenant_tips_run_entries e tenant_tips_roster_entries. total_tip_amount e' lordo, tip_pos_effective_amount e' il POS ricevuto netto, payable_total_amount e' il totale realmente da pagare, payout_status distingue pending/carried/settled.",
            "Per magazzini, inventari, giacenze, rimanenze, scorte o domande del tipo 'cosa abbiamo in casa/in magazzino' usa describe_tenant_schema + run_tenant_query sulle tabelle inventory_warehouses, inventory_latest_items e inventory_latest_lots. Se l'utente chiede chi ha fatto/salvato l'ultimo inventario, usa inventory_warehouses.latest_inventory_created_by_name oppure tenant_inventory_sessions.created_by_name ordinando per created_at DESC. Se un magazzino non ha ancora un inventario datato, inventory_latest_items contiene il contenuto corrente registrato del magazzino come fallback.",
            "Per domande inventario con soglia globale come 'prodotti in casa con meno di 10 unita', 'sotto 10 unita' o 'sotto scorta', aggrega prima tutte le righe di inventory_latest_items per product_name e supplier_name, poi filtra con HAVING SUM(total_equivalent_units) < soglia. Non usare WHERE total_equivalent_units < soglia salvo richiesta esplicita per singolo magazzino.",
            "Per classifiche inventario come 'top 20', 'prodotti con maggior giacenza', 'prodotti con piu unita in casa' o 'prodotti di cui abbiamo piu quantita', aggrega inventory_latest_items per product_name e supplier_name, ordina per SUM(total_equivalent_units) DESC e usa il LIMIT richiesto.",
            "Per cataloghi fornitori separati dal catalogo prodotti del locale usa describe_tenant_schema + run_tenant_query sulle tabelle supplier_catalogs e supplier_catalog_items.",
            "Se l'utente chiede 'chi vende questo', 'quale fornitore ha X piu economico', 'mostrami i cataloghi fornitori per ...', 'nel catalogo Laconi quanto costa X' o confronti di prezzo tra fornitori, NON usare search_products: usa i cataloghi fornitori salvati separati con describe_tenant_schema + run_tenant_query.",
            "Per confronti ordini tipo 'questo mese contro maggio 2025', usa compare_purchase_periods con query vuota se non c'e prodotto/fornitore esplicito. Non mettere parole generiche come allora, questo, mese, contro o confronta dentro query.",
            "Per bolle, fatture, DDT, merce arrivata, righe documento fiscale o domande del tipo 'cosa e arrivato', 'cosa c'e nella bolla', 'quanto vale la merce arrivata' usa describe_tenant_schema + run_tenant_query sulle tabelle tenant_fiscal_documents e tenant_fiscal_document_items.",
            "Per domande tipo 'quanto abbiamo speso/pagato di X nel periodo', usa tenant_fiscal_documents.total_amount come totale IVA inclusa. Non usare ordini_items. Il supplier_name del documento e' il fornitore normalizzato; ignora destinatari/fatturazione nel testo OCR come Sammy Productions.",
            "Per richieste sul consumo tra inventari, come 'quanto abbiamo consumato', 'quanta vodka abbiamo consumato', 'cosa e sceso negli ultimi giorni', usa get_inventory_consumption e non i tool storico ordini.",
            "Per stime di consumo medio/giornaliero basate su ordini storici e inventari, non trattare la frase come ricerca prodotto e NON usare get_inventory_consumption, perche quel tool serve solo a confrontare due inventari gia salvati. Usa dati reali via run_tenant_query: ordini_items/ordini_batches per acquisti, tenant_inventory_sessions/tenant_inventory_session_items o inventory_latest_items per giacenze. Spiega sempre la formula: giacenza iniziale + acquisti - giacenza finale diviso giorni. Se manca la giacenza iniziale, dichiara che la stima e parziale.",
            "Per consumi, stock, andamento o sotto-stock delle prep/preparazioni/homemade usa describe_tenant_schema + run_tenant_query sulle tabelle tenant_homemade_stock_movements, tenant_homemade_stock_items e tenant_homemade_operational_days. consumed_quantity indica consumo reale della prep, added_quantity indica produzione/carico. Le medie giornaliere delle prep vanno divise per i giorni operativi calendario dell'area della prep, non per i soli giorni con decrementi. Non usare tenant_inventory_consumption_product_stats per le prep.",
            "Per prep interne, homemade, ricette batch o domande del tipo 'come faccio 5 litri di sweet sour' usa get_homemade_recipe solo quando l'utente chiede ricetta, ingredienti, dosi o resa.",
            "Per domande su catalogo attuale, disponibilita prodotti, marche usate dal locale e prezzi del catalogo usa di preferenza search_products.",
            "Se l'utente chiede di aggiungere o aggiornare un prodotto, usa upsert_product. Chiedi almeno il nome prodotto e, se possibile, raccogli anche lotto, fornitore, codice, prezzo, iva, categoria e note. Se alcuni dati mancano, il prodotto puo essere salvato comunque e completato dopo.",
            "Se l'utente chiede un confronto storico tra anni o periodi sugli ordini, usa compare_purchase_periods.",
            "Usa get_purchase_overview, get_purchase_history, get_purchase_frequency o get_purchase_batches solo se la richiesta riguarda storico ordini, frequenza acquisti, ultimo ordine, anni specifici, mesi specifici o date passate.",
            "Per richieste ordini generiche senza prodotto o fornitore esplicito, come 'mostrami gli ordini di settembre 2025' oppure confronti come 'agosto 2025 o settembre 2025', NON chiedere una marca o un prodotto: usa query vuota.",
            "Se l'utente scrive follow-up brevi come 'e grey goose?', 'la sky', 'quanto costa quella vodka?' oppure 'me li puoi mostrare tutti?', usa la conversazione recente per capire il riferimento e scegli il tool coerente.",
            "Nei follow-up, eredita il dominio della conversazione precedente salvo cambio esplicito: non cambiare da consumi a giacenze, da timbrature a inventario o da ordini a catalogo solo per una parola ambigua.",
            "Quando costruisci query SQL con termini testuali, filtra solo per parole che identificano davvero prodotto, fornitore, cliente, utente o magazzino. Non usare come filtri parole di frase come 'stima', 'quanto', 'giorno', 'tenendo', 'conto', 'iniziale', 'tralasciamo', numeri di limite o anni.",
            "Per follow-up su ordini o acquisti come 'me li puoi mostrare tutti?', 'mi mostri i singoli ordini?', 'separati', 'a totali', eredita da sola mese, anno, fornitore o prodotto dai messaggi precedenti e usa i tool ordini corretti, non search_products.",
            "Se l'utente chiede di creare un documento o un foglio, usa create_google_workspace_document. Se il file deve riportare dati reali del locale, prima raccogli i dati con i tool corretti e poi crea il file come ultimo tool.",
            "Per file basati su dati reali, non usare parole di comando o destinazione come filtri: ignora termini come crea, salva, sheet, file, drive, cartella, mio, tutte, resoconto, report. Filtra solo per entita reali: prodotto, fornitore, persona, magazzino, area o periodo.",
            "Se devi creare uno sheet sulle mance e l'utente chiede tutte/resoconto/report/storico, usa run_tenant_query su tenant_tips_runs e tenant_tips_run_entries senza filtro staff, salvo che venga nominata una persona specifica.",
            "Se l'utente chiede liste, conteggi, percentuali o semplici calcoli sui dati reali del locale, usa prima i tool giusti e poi rispondi calcolando solo sui risultati ottenuti.",
            "Se il planner vuole usare run_tenant_query e non conosce bene le tabelle disponibili, fai prima describe_tenant_schema nello stesso piano.",
            "Un piano con soltanto describe_tenant_schema e' incompleto e verra' rifiutato.",
            "Non dichiarare mai dati di venduto, incasso o cassa se non esiste ancora una fonte reale collegata. In quel caso spiega chiaramente che quel datasource non e' disponibile.",
            "Se l'utente chiede la lista degli utenti/staff del locale usa list_tenant_users. Per creare o eliminare un sotto-account usa manage_tenant_user.",
            "Se l'utente chiede cartellini, ore lavorate, turni attivi o presenze staff, usa get_timeclock_summary. Per analisi SQL piu libere sul cartellino puoi usare anche describe_tenant_schema + run_tenant_query.",
            "Se l'utente chiede giacenze inventario o cosa c'e nei magazzini, NON usare get_purchase_overview o get_purchase_history: usa i dati inventario del magazzino, con ultimo inventario salvato e fallback al contenuto corrente registrato se manca un inventario datato.",
            "Se l'utente chiede totali inventario 'in casa' o senza indicare un magazzino, considera il totale aggregato su tutti i magazzini, non le singole righe per magazzino.",
            "Se l'utente risponde con follow-up brevi come 'AU VODKA', 'grey goose' o 'quella vodka' dopo una domanda su consumi inventario, eredita il contesto consumo inventario e non ricominciare da capo.",
            "Se l'utente chiede di vedere o cambiare le impostazioni di un modulo (prenotazioni, whatsapp, LLM, fatture) usa get_module_settings o update_module_settings.",
            "Se l'utente chiede di aggiornare il nome del locale, l'indirizzo, il telefono o il numero WhatsApp usa update_venue_profile.",
            "Se l'utente chiede l'elenco delle fatture o dei documenti fiscali usa list_fiscal_documents.",
            "Per liste prenotazioni piu dettagliate o filtrate per data usa list_reservations, non get_reservations_snapshot.",
            "Non inventare mai dati o azioni.",
            "Se la richiesta e' un semplice saluto o una frase sociale senza bisogno di dati, usa mode=reply.",
            "Se per completare un'azione servono piu dettagli o la richiesta e' ambigua, usa mode=reply e chiedi una sola chiarificazione chiara.",
            "Per creare, modificare o cancellare prenotazioni usa i tool dedicati, mai una reply inventata.",
            "Per domande su sale interne/esterne, piantina, coperti massimi, tavoli occupati o posti liberi usa get_reservations_snapshot.",
            "Non dedurre o inventare mai orari di apertura del locale. Se mancano dati affidabili sugli orari o la richiesta contiene orari ambigui, chiedi chiarimento senza fare supposizioni.",
            "Per scrivere note o obiettivi usa i tool dedicati, mai dire che l'azione e' stata eseguita se non hai chiamato il tool.",
            "Per date relative come oggi, domani, stasera, settimana prossima, mese prossimo o anno corrente usa il Contesto runtime fornito nel messaggio utente e risolvi sempre date esplicite nel formato YYYY-MM-DD.",
            "Per ordini sospesi: 'crea' normalmente -> operation='set'; 'aggiungi' -> operation='add'.",
            "La data/ora corrente e il locale attivo sono forniti nel Contesto runtime del messaggio utente: non inserirli nel prompt statico.",
            f"Tool disponibili e argomenti: {_planner_tool_catalog_summary()}",
            f"Schema rapido query layer: {_planner_schema_hints()}",
            "Per run_tenant_query scegli LIMIT piccoli e coerenti con la richiesta: 1 per totali singoli, N per top/lista richiesta, mai 5000 se non serve davvero una lista completa.",
            'Restituisci SOLO JSON con questa forma: {"mode":"reply","reply":"...","tool_calls":[],"confidence":0.0-1.0,"needs_clarification":false} oppure {"mode":"tool","reply":null,"tool_calls":[{"tool":"nome_tool","arguments":{...}}],"confidence":0.0-1.0,"needs_clarification":false}.',
        ]
    )


def _planner_runtime_context(session: SessionIdentity) -> str:
    now = _now_in_timezone()
    context = _get_locale_profile(session)
    return "\n".join(
        [
            f"Data e ora correnti: {now.isoformat()} ({get_settings().assistant_timezone})",
            f"Data corrente ISO: {now.date().isoformat()}",
            f"Locale attivo: {json.dumps(context, ensure_ascii=False)}",
        ]
    )


def _build_recent_conversation(conversation: list[dict[str, str]]) -> str:
    if not conversation:
        return "(nessun messaggio precedente)"
    lines = []
    for message in conversation[-10:]:
        role = "Utente" if message.get("role") == "user" else "Assistente"
        lines.append(f"{role}: {message.get('content', '').strip()}")
    return "\n".join(lines)


def _coerce_optional_year(value: object) -> int | None:
    try:
        year = int(value)
    except (TypeError, ValueError):
        return None
    return year if 2020 <= year <= 2100 else None


def _coerce_optional_month(value: object) -> int | None:
    try:
        month = int(value)
    except (TypeError, ValueError):
        return None
    return month if 1 <= month <= 12 else None


def _coerce_optional_iso_date(value: object) -> str | None:
    if not isinstance(value, str):
        return None
    raw = value.strip()
    if not raw:
        return None
    try:
        return date.fromisoformat(raw).isoformat()
    except ValueError:
        return None


def _normalize_pending_product_state(payload: object) -> dict[str, object]:
    if not isinstance(payload, dict):
        return {}
    normalized: dict[str, object] = {}
    operation = payload.get("operation")
    if isinstance(operation, str) and operation.strip() in {"upsert", "delete"}:
        normalized["operation"] = operation.strip()
    for key in ("product_name", "lot_code", "supplier_name", "product_code", "category", "notes"):
        value = payload.get(key)
        if isinstance(value, str) and value.strip():
            normalized[key] = value.strip()
    for key in ("final_price_vat", "vat_rate", "units_per_pack", "weight_kg", "unit_price_per_kg"):
        value = _coerce_positive_float(payload.get(key))
        if value is not None:
            normalized[key] = value
    return normalized


def _normalize_pending_sales_goal_state(payload: object) -> dict[str, object]:
    if not isinstance(payload, dict):
        return {}
    normalized: dict[str, object] = {}
    operation = payload.get("operation")
    if isinstance(operation, str) and operation.strip() in {"upsert", "delete"}:
        normalized["operation"] = operation.strip()
    for key in ("name", "goal_type", "description", "product_match", "secondary_product_match", "supplier_match", "unit_label"):
        value = payload.get(key)
        if isinstance(value, str) and value.strip():
            normalized[key] = value.strip()
    for key in ("year",):
        value = _coerce_optional_year(payload.get(key))
        if value is not None:
            normalized[key] = value
    for key in ("target", "secondary_target"):
        value = _coerce_positive_float(payload.get(key))
        if value is not None:
            normalized[key] = value
    return normalized


def _normalize_last_product_write_state(payload: object) -> dict[str, object]:
    if not isinstance(payload, dict):
        return {}
    normalized: dict[str, object] = {}
    status = payload.get("status")
    if isinstance(status, str) and status.strip() in {"created", "updated", "deleted", "not_found", "clarification_required"}:
        normalized["status"] = status.strip()
    for key in ("product_name", "lot_code", "supplier_name"):
        value = payload.get(key)
        if isinstance(value, str) and value.strip():
            normalized[key] = value.strip()
    return normalized


def _normalize_home_thread_state(thread_state: dict[str, object] | None) -> dict[str, object]:
    if not isinstance(thread_state, dict):
        return {}

    state: dict[str, object] = {}
    for key in (
        "catalog_query",
        "purchase_query",
        "purchase_view",
        "inventory_consumption_estimation_mode",
        "inventory_consumption_estimation_query",
        "last_tool",
        "purchase_first_ordered_at",
        "purchase_last_ordered_at",
        "fiscal_spend_query",
        "timeclock_query_text",
        "timeclock_scope",
        "timeclock_resolved_user_id",
        "timeclock_resolved_user_name",
        "timeclock_resolved_username",
        "timeclock_resolved_email",
    ):
        value = thread_state.get(key)
        if isinstance(value, str) and value.strip():
            state[key] = value.strip()

    purchase_query = str(state.get("purchase_query") or "").strip()
    if purchase_query and _is_generic_purchase_followup_query(purchase_query):
        state.pop("purchase_query", None)
        comparison = thread_state.get("comparison")
        if isinstance(comparison, dict) and str(comparison.get("query") or "").strip() == purchase_query:
            cleaned_comparison = dict(comparison)
            cleaned_comparison["query"] = ""
            thread_state = {**thread_state, "comparison": cleaned_comparison}

    for key in ("purchase_year", "fiscal_spend_year"):
        value = _coerce_optional_year(thread_state.get(key))
        if value is not None:
            state[key] = value

    for key in ("inventory_consumption_estimation_purchase_year", "inventory_consumption_estimation_inventory_year"):
        value = _coerce_optional_year(thread_state.get(key))
        if value is not None:
            state[key] = value

    for key in ("purchase_month", "fiscal_spend_month"):
        value = _coerce_optional_month(thread_state.get(key))
        if value is not None:
            state[key] = value

    for key in (
        "purchase_start_date",
        "purchase_end_date",
        "fiscal_spend_start_date",
        "fiscal_spend_end_date",
        "reservation_date",
        "timeclock_target_date",
        "timeclock_start_date",
        "timeclock_end_date",
    ):
        value = _coerce_optional_iso_date(thread_state.get(key))
        if value is not None:
            state[key] = value

    if str(state.get("timeclock_scope") or "") not in {"today", "week", "all", "active"}:
        state.pop("timeclock_scope", None)

    if isinstance(thread_state.get("timeclock_include_entries"), bool):
        state["timeclock_include_entries"] = thread_state["timeclock_include_entries"]

    comparison_raw = thread_state.get("comparison")
    if isinstance(comparison_raw, dict):
        comparison: dict[str, object] = {}
        query = comparison_raw.get("query")
        if isinstance(query, str) and query.strip():
            comparison["query"] = query.strip()
        for key in ("primary_year", "secondary_year"):
            value = _coerce_optional_year(comparison_raw.get(key))
            if value is not None:
                comparison[key] = value
        for key in ("primary_month", "secondary_month"):
            value = _coerce_optional_month(comparison_raw.get(key))
            if value is not None:
                comparison[key] = value
        focus_hint = comparison_raw.get("focus_hint")
        if isinstance(focus_hint, str) and focus_hint.strip() in {"products", "orders", "quantity", "amount"}:
            comparison["focus_hint"] = focus_hint.strip()
        if isinstance(comparison_raw.get("percentage_requested"), bool):
            comparison["percentage_requested"] = comparison_raw["percentage_requested"]
        if comparison:
            state["comparison"] = comparison

    pending_action = thread_state.get("pending_action")
    if isinstance(pending_action, str) and pending_action.strip() in {"product_write", "sales_goal_write"}:
        state["pending_action"] = pending_action.strip()

    pending_product = _normalize_pending_product_state(thread_state.get("pending_product"))
    if pending_product:
        state["pending_product"] = pending_product

    pending_sales_goal = _normalize_pending_sales_goal_state(thread_state.get("pending_sales_goal"))
    if pending_sales_goal:
        state["pending_sales_goal"] = pending_sales_goal

    last_product_write = _normalize_last_product_write_state(thread_state.get("last_product_write"))
    if last_product_write:
        state["last_product_write"] = last_product_write

    return state


def _build_home_thread_state_summary(thread_state: dict[str, object] | None) -> str:
    state = _normalize_home_thread_state(thread_state)
    if not state:
        return "(nessuno)"

    summary: dict[str, object] = {}
    if state.get("catalog_query"):
        summary["catalog_query"] = state["catalog_query"]
    if state.get("purchase_view") or state.get("purchase_query") or state.get("purchase_year") or state.get("purchase_month"):
        summary["purchase"] = {
            "view": state.get("purchase_view"),
            "query": state.get("purchase_query") or "",
            "year": state.get("purchase_year"),
            "month": state.get("purchase_month"),
            "start_date": state.get("purchase_start_date"),
            "end_date": state.get("purchase_end_date"),
            "first_ordered_at": state.get("purchase_first_ordered_at"),
            "last_ordered_at": state.get("purchase_last_ordered_at"),
        }
    if isinstance(state.get("comparison"), dict):
        summary["comparison"] = state["comparison"]
    if state.get("fiscal_spend_query") or state.get("fiscal_spend_year") or state.get("fiscal_spend_month"):
        summary["fiscal_spend"] = {
            "query": state.get("fiscal_spend_query") or "",
            "year": state.get("fiscal_spend_year"),
            "month": state.get("fiscal_spend_month"),
            "start_date": state.get("fiscal_spend_start_date"),
            "end_date": state.get("fiscal_spend_end_date"),
        }
    if state.get("inventory_consumption_estimation_mode"):
        summary["inventory_consumption_estimation"] = {
            "mode": state.get("inventory_consumption_estimation_mode"),
            "query": state.get("inventory_consumption_estimation_query") or "",
            "purchase_year": state.get("inventory_consumption_estimation_purchase_year"),
            "inventory_year": state.get("inventory_consumption_estimation_inventory_year"),
        }
    if state.get("reservation_date"):
        summary["reservations"] = {"date": state.get("reservation_date")}
    if state.get("timeclock_query_text") or state.get("timeclock_scope") or state.get("timeclock_resolved_user_name"):
        summary["timeclock"] = {
            "query_text": state.get("timeclock_query_text") or "",
            "scope": state.get("timeclock_scope"),
            "target_date": state.get("timeclock_target_date"),
            "start_date": state.get("timeclock_start_date"),
            "end_date": state.get("timeclock_end_date"),
            "include_entries": state.get("timeclock_include_entries"),
            "resolved_user": {
                "id": state.get("timeclock_resolved_user_id"),
                "name": state.get("timeclock_resolved_user_name"),
                "username": state.get("timeclock_resolved_username"),
                "email": state.get("timeclock_resolved_email"),
            },
        }
    pending_action = state.get("pending_action")
    if pending_action == "product_write":
        summary["pending_action"] = {
            "type": "product_write",
            "payload": state.get("pending_product") or {},
        }
    elif pending_action == "sales_goal_write":
        summary["pending_action"] = {
            "type": "sales_goal_write",
            "payload": state.get("pending_sales_goal") or {},
        }
    if state.get("last_tool"):
        summary["last_tool"] = state["last_tool"]
    if isinstance(state.get("last_product_write"), dict):
        summary["last_product_write"] = state["last_product_write"]
    return json.dumps(summary, ensure_ascii=False)


def _message_has_explicit_purchase_period(message: str) -> bool:
    return any(
        (
            _extract_reference_year(message) is not None,
            _extract_reference_month(message) is not None,
            _extract_reference_week_range(message) is not None,
            bool(_extract_reference_periods(message)),
            len(_extract_reference_years(message)) >= 2,
        )
    )


def _should_inherit_purchase_context(message: str, normalized: str) -> bool:
    return any(
        (
            _is_short_followup_query(message),
            normalized.startswith("e "),
            _is_purchase_followup_request(normalized),
            _is_purchase_expand_followup_request(normalized),
            _is_purchase_comparison_request(message, normalized),
        )
    )


def _month_date_range(year: int, month: int, *, until_today: bool = False) -> tuple[date, date]:
    start_date = date(year, month, 1)
    end_date = date(year, month, monthrange(year, month)[1])
    if until_today:
        today = _today_in_timezone()
        if today.year == year and today.month == month:
            end_date = today
    return start_date, end_date


def _shift_year_month(year: int, month: int, offset_months: int) -> tuple[int, int]:
    absolute_month = (year * 12) + (month - 1) + offset_months
    return absolute_month // 12, (absolute_month % 12) + 1


def _week_date_range(reference_day: date, *, until_reference_day: bool = False) -> tuple[date, date]:
    start_date = reference_day - timedelta(days=reference_day.weekday())
    end_date = start_date + timedelta(days=6)
    if until_reference_day:
        end_date = reference_day
    return start_date, end_date


def _timeclock_relative_period_from_message(message: str, normalized: str) -> tuple[str, str | None, str | None, str | None] | None:
    today = _today_in_timezone()

    if (
        _contains_normalized_word(normalized, "adesso", "ora")
        or any(fragment in normalized for fragment in ("in turno", "turni attivi", "attivo"))
    ):
        return "active", None, None, None

    explicit_date = _extract_explicit_date(message)
    if explicit_date is None and _contains_normalized_word(normalized, "ieri"):
        explicit_date = today - timedelta(days=1)
    if explicit_date is not None:
        return "today", explicit_date.isoformat(), None, None

    week_range = _extract_reference_week_range(message)
    if week_range is not None:
        start_date, end_exclusive = week_range
        end_date = end_exclusive - timedelta(days=1)
        return "week", None, start_date.isoformat(), end_date.isoformat()

    if "settiman" in normalized:
        if any(fragment in normalized for fragment in ("settimana prossima", "prossima settimana", "settiman prossim")):
            start_date, end_date = _week_date_range(today + timedelta(days=7))
        elif any(fragment in normalized for fragment in ("settimana scorsa", "scorsa settimana", "settimana passata", "passata settimana")):
            start_date, end_date = _week_date_range(today - timedelta(days=7))
        else:
            start_date, end_date = _week_date_range(today, until_reference_day=True)
        return "week", None, start_date.isoformat(), end_date.isoformat()

    if "mese" in normalized:
        if any(fragment in normalized for fragment in ("mese prossimo", "prossimo mese", "mes prossim")):
            year, month = _shift_year_month(today.year, today.month, 1)
            start_date, end_date = _month_date_range(year, month)
        elif any(fragment in normalized for fragment in ("mese scorso", "scorso mese", "mese passato", "passato mese")):
            year, month = _shift_year_month(today.year, today.month, -1)
            start_date, end_date = _month_date_range(year, month)
        else:
            start_date, end_date = _month_date_range(today.year, today.month, until_today=True)
        return "all", None, start_date.isoformat(), end_date.isoformat()

    month = _extract_reference_month(message)
    year = _extract_reference_year(message)
    if month is not None:
        if year is None:
            year = today.year
        start_date, end_date = _month_date_range(year, month)
        return "all", None, start_date.isoformat(), end_date.isoformat()

    if year is not None:
        end_date = today if year == today.year and any(fragment in normalized for fragment in ("quest anno", "questo anno")) else date(year, 12, 31)
        return "all", None, f"{year:04d}-01-01", end_date.isoformat()

    return None


def _timeclock_period_from_message(message: str, normalized: str) -> tuple[str, str | None, str | None, str | None]:
    relative_period = _timeclock_relative_period_from_message(message, normalized)
    if relative_period is not None:
        return relative_period

    if any(fragment in normalized for fragment in ("storico", "in generale", "generale", "tutto", "tutti i turni", "tutto il cartellino")):
        return "all", None, None, None

    return "today", None, None, None


def _message_has_explicit_timeclock_period(message: str, normalized: str) -> bool:
    if _timeclock_relative_period_from_message(message, normalized) is not None:
        return True
    return any(
        fragment in normalized
        for fragment in ("storico", "in generale", "generale", "tutti i turni", "tutto il cartellino")
    )


def _is_generic_timeclock_team_query(value: str) -> bool:
    tokens = [token for token in _normalize_text(value).split() if token]
    if not tokens:
        return False
    generic_tokens = {
        "i",
        "il",
        "la",
        "le",
        "gli",
        "lo",
        "ragazzi",
        "ragazzo",
        "staff",
        "dipendente",
        "dipendenti",
        "personale",
        "team",
        "tutti",
        "tutte",
        "tutto",
    }
    return all(token in generic_tokens for token in tokens)


def _normalize_timeclock_tool_arguments(
    message: str,
    normalized: str,
    source_arguments: dict[str, object],
) -> dict[str, object]:
    arguments = dict(source_arguments)
    source_scope = str(arguments.get("scope") or "").strip()
    source_scope = source_scope if source_scope in {"today", "week", "all", "active"} else ""
    source_target_date = _coerce_optional_iso_date(arguments.get("target_date"))
    source_start_date = _coerce_optional_iso_date(arguments.get("start_date"))
    source_end_date = _coerce_optional_iso_date(arguments.get("end_date"))
    has_source_period = (
        source_scope in {"week", "all", "active"}
        or source_target_date is not None
        or source_start_date is not None
        or source_end_date is not None
    )
    if _message_has_explicit_timeclock_period(message, normalized) or not has_source_period:
        scope, target_date, start_date, end_date = _timeclock_period_from_message(message, normalized)
    else:
        scope = source_scope or "today"
        target_date = source_target_date
        start_date = source_start_date
        end_date = source_end_date
    arguments["scope"] = scope
    arguments["target_date"] = target_date
    arguments["start_date"] = start_date
    arguments["end_date"] = end_date
    query_text = str(arguments.get("query_text") or "").strip()
    if _is_generic_timeclock_team_query(query_text):
        query_text = ""
    arguments["query_text"] = query_text or (message if _is_timeclock_request(normalized) else "")
    try:
        limit = int(arguments.get("limit") or _CHAT_LIST_LIMIT)
    except (TypeError, ValueError):
        limit = _CHAT_LIST_LIMIT
    arguments["limit"] = max(1, min(limit, _CHAT_LIST_LIMIT))
    if not isinstance(arguments.get("include_entries"), bool):
        arguments["include_entries"] = any(
            fragment in normalized
            for fragment in ("timbrature", "dettaglio", "dettagli", "mostra", "mostrami", "elenco", "storico", "singolarmente", "singoli", "singolo")
        )
    return arguments


def _is_timeclock_contextual_followup_request(message: str, normalized: str) -> bool:
    if _is_timeclock_request(normalized):
        return True
    if not (_is_short_followup_query(message) or normalized.startswith("e ")):
        return False
    if _extract_reference_year(message) is not None or _extract_reference_month(message) is not None:
        return True
    if _extract_explicit_date(message) is not None:
        return True
    return any(
        fragment in normalized
        for fragment in (
            "ieri",
            "oggi",
            "settimana",
            "mese",
            "anno",
            "storico",
            "in generale",
            "generale",
            "dettaglio",
            "dettagli",
            "mostra",
            "mostrami",
            "mostri",
            "singolarmente",
            "singoli",
            "singolo",
            "timbrature",
            "tutto",
            "tutti",
        )
    )


def _has_explicit_non_timeclock_domain(message: str, normalized: str) -> bool:
    if _is_timeclock_request(normalized):
        return False

    product_query = _extract_product_query(message)
    catalog_query = _extract_catalog_query(message)
    return any(
        (
            _is_reservation_subject_request(normalized) or _is_reservation_write_request(normalized),
            _is_tips_request(message, normalized),
            _is_inventory_request(message, normalized),
            _is_homemade_request(message, normalized),
            _is_fiscal_documents_request(normalized),
            _is_tenant_user_list_request(normalized),
            _is_module_settings_read_request(normalized),
            _is_sales_goal_read_request(normalized) or _is_sales_goal_write_request(normalized),
            _contains_normalized_word(normalized, "nota", "note"),
            _is_supplier_catalog_request(normalized),
            bool(catalog_query and _is_catalog_request(message, normalized, catalog_query)),
            _is_catalog_request(message, normalized, product_query),
            any(keyword in normalized for keyword in _ORDERS_KEYWORDS),
        )
    )


def _build_contextual_timeclock_tool_calls(
    message: str,
    conversation: list[dict[str, str]],
    thread_state: dict[str, object] | None = None,
) -> list[PlannedToolCall]:
    state = _normalize_home_thread_state(thread_state)
    last_user_message = _latest_user_message(conversation)
    latest_timeclock_message = _latest_timeclock_request_message(conversation)
    normalized_current = _normalize_text(message)
    normalized_previous = _normalize_text(last_user_message)

    if _has_explicit_non_timeclock_domain(message, normalized_current):
        return []

    state_query_text = str(state.get("timeclock_query_text") or "").strip()
    previous_query_text = latest_timeclock_message.strip() or (last_user_message.strip() if _is_timeclock_request(normalized_previous) else "")
    state_is_timeclock = str(state.get("last_tool") or "") == "get_timeclock_summary"
    if state_is_timeclock and not latest_timeclock_message and last_user_message and _has_explicit_non_timeclock_domain(last_user_message, normalized_previous):
        state_is_timeclock = False
    inherited_query_text = state_query_text if state_is_timeclock and state_query_text else previous_query_text
    previous_was_timeclock = state_is_timeclock or bool(previous_query_text)
    if not previous_was_timeclock or not inherited_query_text:
        return []
    if not _is_timeclock_contextual_followup_request(message, normalized_current):
        return []

    explicit_period = _message_has_explicit_timeclock_period(message, normalized_current)
    state_scope = str(state.get("timeclock_scope") or "").strip()
    state_scope = state_scope if state_scope in {"today", "week", "all", "active"} else ""
    state_target_date = _coerce_optional_iso_date(state.get("timeclock_target_date"))
    state_start_date = _coerce_optional_iso_date(state.get("timeclock_start_date"))
    state_end_date = _coerce_optional_iso_date(state.get("timeclock_end_date"))
    state_has_meaningful_period = (
        state_scope in {"week", "all", "active"}
        or state_target_date is not None
        or state_start_date is not None
        or state_end_date is not None
    )
    if explicit_period:
        scope, target_date, start_date, end_date = _timeclock_period_from_message(message, normalized_current)
    elif state_is_timeclock and state_has_meaningful_period:
        scope = state_scope or ("all" if state_start_date or state_end_date else "today")
        target_date = state_target_date
        start_date = state_start_date
        end_date = state_end_date
    elif latest_timeclock_message and _message_has_explicit_timeclock_period(latest_timeclock_message, _normalize_text(latest_timeclock_message)):
        scope, target_date, start_date, end_date = _timeclock_period_from_message(
            latest_timeclock_message,
            _normalize_text(latest_timeclock_message),
        )
    else:
        scope, target_date, start_date, end_date = _timeclock_period_from_message(message, normalized_current)
    include_entries = bool(state.get("timeclock_include_entries")) or any(
        fragment in normalized_current
        for fragment in ("timbrature", "dettaglio", "dettagli", "mostra", "mostrami", "elenco", "storico", "singolarmente", "singoli", "singolo")
    )
    query_text = message if _is_timeclock_request(normalized_current) else inherited_query_text

    return [
        PlannedToolCall(
            tool="get_timeclock_summary",
            arguments={
                "query_text": query_text,
                "scope": scope,
                "target_date": target_date,
                "start_date": start_date,
                "end_date": end_date,
                "limit": _CHAT_LIST_LIMIT,
                "include_entries": include_entries,
            },
        )
    ]


def _apply_home_thread_state_to_tool_calls(
    message: str,
    tool_calls: list[PlannedToolCall],
    *,
    conversation: list[dict[str, str]],
    thread_state: dict[str, object] | None = None,
) -> list[PlannedToolCall]:
    state = _normalize_home_thread_state(thread_state)
    if not state:
        return tool_calls

    normalized = _normalize_text(message)
    explicit_purchase_query = _extract_purchase_query(message).strip()
    explicit_product_query = _extract_product_query(message).strip()
    explicit_year = _extract_reference_year(message)
    explicit_month = _extract_reference_month(message)
    explicit_periods = _extract_reference_periods(message)
    explicit_years = _extract_reference_years(message)
    week_range = _extract_reference_week_range(message)
    wants_latest_batches = _is_latest_batches_request(normalized)
    chronological_order_rank = _extract_chronological_order_rank(message)
    latest_batches_limit = _extract_requested_latest_batches_limit(message)
    should_inherit_purchase = _should_inherit_purchase_context(message, normalized)
    if (
        not explicit_purchase_query
        and _contains_normalized_word(normalized, "ordine", "ordini")
        and (wants_latest_batches or chronological_order_rank is not None)
    ):
        should_inherit_purchase = False
    state_purchase_query = str(state.get("purchase_query") or "").strip()
    state_purchase_view = str(state.get("purchase_view") or "").strip()
    state_purchase_year = _coerce_optional_year(state.get("purchase_year"))
    state_purchase_month = _coerce_optional_month(state.get("purchase_month"))
    state_start_date = _coerce_optional_iso_date(state.get("purchase_start_date"))
    state_end_date = _coerce_optional_iso_date(state.get("purchase_end_date"))
    comparison_state = state.get("comparison") if isinstance(state.get("comparison"), dict) else {}
    state_catalog_query = str(state.get("catalog_query") or "").strip()
    state_timeclock_query = str(state.get("timeclock_query_text") or "").strip()
    state_estimation_context = bool(state.get("inventory_consumption_estimation_mode"))
    if state_estimation_context and _is_homemade_context_switch(normalized):
        inherited_query = str(state.get("inventory_consumption_estimation_query") or "").strip()
        return [_build_homemade_stock_consumption_tool_call(message, query_override=inherited_query)]
    if state_estimation_context and (
        _is_short_followup_query(message)
        or _contains_normalized_word(normalized, "quanto", "quanta", "quanti", "quante")
        or "consum" in normalized
        or "giornal" in normalized
        or " al giorno" in f" {normalized}"
        or "non come giacenza" in normalized
    ):
        estimation_query = _extract_inventory_consumption_estimation_query(message) or _extract_inventory_query(message)
        if estimation_query:
            return [
                _build_inventory_consumption_estimation_tool_call(
                    message,
                    query_override=estimation_query,
                    thread_state=state,
                )
            ]

    normalized_calls: list[PlannedToolCall] = []
    for tool_call in tool_calls:
        arguments = dict(tool_call.arguments)
        tool_name = tool_call.tool

        if tool_name == "get_timeclock_summary":
            arguments = _normalize_timeclock_tool_arguments(message, normalized, arguments)
            has_current_timeclock_subject = _is_timeclock_request(normalized)
            contextual_timeclock_followup = _is_timeclock_contextual_followup_request(message, normalized)
            explicit_timeclock_period = _message_has_explicit_timeclock_period(message, normalized)
            arguments_target_date = _coerce_optional_iso_date(arguments.get("target_date"))
            arguments_start_date = _coerce_optional_iso_date(arguments.get("start_date"))
            arguments_end_date = _coerce_optional_iso_date(arguments.get("end_date"))
            arguments_scope = str(arguments.get("scope") or "").strip()
            arguments_has_meaningful_period = (
                arguments_scope in {"week", "all", "active"}
                or arguments_target_date is not None
                or arguments_start_date is not None
                or arguments_end_date is not None
            )
            if state_timeclock_query and (not str(arguments.get("query_text") or "").strip() or (contextual_timeclock_followup and not has_current_timeclock_subject)):
                arguments["query_text"] = state_timeclock_query
            if contextual_timeclock_followup and not has_current_timeclock_subject and bool(state.get("timeclock_include_entries")):
                arguments["include_entries"] = True
            if contextual_timeclock_followup and not explicit_timeclock_period and not arguments_has_meaningful_period:
                state_timeclock_scope = str(state.get("timeclock_scope") or "").strip()
                state_timeclock_scope = state_timeclock_scope if state_timeclock_scope in {"today", "week", "all", "active"} else ""
                state_timeclock_target_date = _coerce_optional_iso_date(state.get("timeclock_target_date"))
                state_timeclock_start_date = _coerce_optional_iso_date(state.get("timeclock_start_date"))
                state_timeclock_end_date = _coerce_optional_iso_date(state.get("timeclock_end_date"))
                state_has_meaningful_period = (
                    state_timeclock_scope in {"week", "all", "active"}
                    or state_timeclock_target_date is not None
                    or state_timeclock_start_date is not None
                    or state_timeclock_end_date is not None
                )
                if state_has_meaningful_period:
                    arguments["scope"] = state_timeclock_scope or ("all" if state_timeclock_start_date or state_timeclock_end_date else "today")
                    arguments["target_date"] = state_timeclock_target_date
                    arguments["start_date"] = state_timeclock_start_date
                    arguments["end_date"] = state_timeclock_end_date
            normalized_calls.append(PlannedToolCall(tool=tool_name, arguments=arguments))
            continue

        if tool_name in {"get_purchase_overview", "get_purchase_history", "get_purchase_batches", "get_purchase_frequency", "search_products"} and _is_missing_price_variants_followup_request(normalized, conversation):
            effective_purchase_query = (
                ""
                if _is_generic_purchase_followup_query(state_purchase_query)
                else state_purchase_query
            ) or _latest_purchase_subject_query(conversation) or _latest_purchase_overview_query(conversation)
            if effective_purchase_query:
                normalized_calls.append(
                    PlannedToolCall(
                        tool="get_purchase_overview",
                        arguments={
                            "query": effective_purchase_query,
                            "year": state_purchase_year,
                            "month": state_purchase_month,
                            "start_date": state_start_date,
                            "end_date": state_end_date,
                            "limit": _CHAT_LIST_LIMIT,
                        },
                    )
                )
                continue

        if tool_name == "search_products":
            if _is_purchase_expand_followup_request(normalized) and state_purchase_query and state_purchase_view == "get_purchase_overview":
                normalized_calls.append(
                    PlannedToolCall(
                        tool="get_purchase_overview",
                        arguments={
                            "query": state_purchase_query,
                            "year": state_purchase_year,
                            "month": state_purchase_month,
                            "start_date": state_start_date,
                            "end_date": state_end_date,
                            "limit": _CHAT_LIST_LIMIT,
                        },
                    )
                )
                continue
            if _is_purchase_followup_request(normalized) and state_purchase_view in {"get_purchase_overview", "get_purchase_history", "get_purchase_batches"}:
                normalized_calls.append(
                    PlannedToolCall(
                        tool="get_purchase_batches",
                        arguments={
                            "query": explicit_purchase_query or state_purchase_query,
                            "year": explicit_year or state_purchase_year,
                            "month": explicit_month or state_purchase_month,
                            "start_date": week_range[0].isoformat() if week_range is not None else state_start_date,
                            "end_date": week_range[1].isoformat() if week_range is not None else state_end_date,
                            "limit": _CHAT_LIST_LIMIT,
                        },
                    )
                )
                continue
            if (
                not explicit_product_query
                and state_catalog_query
                and (
                    _is_lowest_price_request(normalized)
                    or _is_price_per_weight_request(normalized)
                    or _is_missing_catalog_price_request(normalized)
                    or _is_price_request(normalized)
                    or _is_catalog_search_followup_request(normalized)
                    or _is_short_followup_query(message)
                )
            ):
                arguments["query"] = state_catalog_query
            if _is_missing_catalog_price_request(normalized):
                arguments["limit"] = _CHAT_LIST_LIMIT
            normalized_calls.append(PlannedToolCall(tool=tool_name, arguments=arguments))
            continue

        if tool_name in {"get_purchase_overview", "get_purchase_frequency", "get_purchase_batches", "get_purchase_history"}:
            if explicit_purchase_query:
                arguments["query"] = explicit_purchase_query
            elif should_inherit_purchase and state_purchase_query:
                arguments["query"] = state_purchase_query
            elif _contains_normalized_word(normalized, "ordine", "ordini") or _contains_normalized_word(normalized, "speso", "spesa"):
                arguments["query"] = ""

            if wants_latest_batches:
                arguments["limit"] = latest_batches_limit

            if wants_latest_batches and tool_name == "get_purchase_batches" and not _message_has_explicit_purchase_period(message):
                arguments["year"] = None
                arguments["month"] = None
                arguments["start_date"] = None
                arguments["end_date"] = None
            elif week_range is None and not _message_has_explicit_purchase_period(message) and should_inherit_purchase:
                if state_purchase_year is not None:
                    arguments["year"] = state_purchase_year
                if state_purchase_month is not None:
                    arguments["month"] = state_purchase_month
                if state_start_date is not None:
                    arguments["start_date"] = state_start_date
                if state_end_date is not None:
                    arguments["end_date"] = state_end_date

            normalized_calls.append(PlannedToolCall(tool=tool_name, arguments=arguments))
            continue

        if tool_name == "compare_purchase_periods":
            comparison_query = explicit_purchase_query
            if not comparison_query and should_inherit_purchase:
                comparison_query = str(comparison_state.get("query") or state_purchase_query or "").strip()
            arguments["query"] = comparison_query

            if explicit_periods and len(explicit_periods) >= 2:
                fallback_year = (
                    _coerce_optional_year(comparison_state.get("primary_year"))
                    or _coerce_optional_year(comparison_state.get("secondary_year"))
                    or state_purchase_year
                    or explicit_year
                )
                primary_year = explicit_periods[0][0] or fallback_year
                secondary_year = explicit_periods[1][0] or fallback_year
                if primary_year is not None:
                    arguments["primary_year"] = primary_year
                if secondary_year is not None:
                    arguments["secondary_year"] = secondary_year
                arguments["primary_month"] = explicit_periods[0][1]
                arguments["secondary_month"] = explicit_periods[1][1]
            elif len(explicit_years) >= 2:
                arguments["primary_year"] = explicit_years[0]
                arguments["secondary_year"] = explicit_years[1]
            elif should_inherit_purchase and comparison_state:
                for key in ("primary_year", "primary_month", "secondary_year", "secondary_month"):
                    value = comparison_state.get(key)
                    if value is not None:
                        arguments[key] = value

            if not arguments.get("focus_hint") and should_inherit_purchase:
                focus_hint = comparison_state.get("focus_hint")
                if isinstance(focus_hint, str) and focus_hint in {"products", "orders", "quantity", "amount"}:
                    arguments["focus_hint"] = focus_hint
            if not isinstance(arguments.get("percentage_requested"), bool) and should_inherit_purchase:
                if isinstance(comparison_state.get("percentage_requested"), bool):
                    arguments["percentage_requested"] = comparison_state["percentage_requested"]

            normalized_calls.append(PlannedToolCall(tool=tool_name, arguments=arguments))
            continue

        normalized_calls.append(PlannedToolCall(tool=tool_name, arguments=arguments))
    return normalized_calls


def _derive_home_thread_state(
    *,
    previous_state: dict[str, object] | None,
    message: str,
    executed_tool_calls: list[PlannedToolCall],
    tool_results: list[dict[str, object]],
    route: str | None = None,
    conversation: list[dict[str, str]] | None = None,
) -> dict[str, object]:
    state = _normalize_home_thread_state(previous_state)
    normalized = _normalize_text(message)
    recent_conversation = conversation or []

    if route == "deterministic-product-clarification":
        if _is_relative_new_product_delete_request(normalized):
            last_product_write = state.get("last_product_write") if isinstance(state.get("last_product_write"), dict) else {}
            if str(last_product_write.get("status") or "").strip() != "created":
                state.pop("pending_action", None)
                state.pop("pending_product", None)
                state["last_tool"] = "upsert_product"
                return state
        state["pending_action"] = "product_write"
        state["pending_product"] = _merge_product_write_fragments(message, recent_conversation, state)
        state["last_tool"] = "upsert_product"
        return state

    if route == "deterministic-pack-size-clarification":
        context = _latest_pack_size_context(recent_conversation) or _latest_pack_size_context_from_state(state)
        state["pending_action"] = "product_write"
        pending_product = _normalize_pending_product_state(state.get("pending_product"))
        if context is not None:
            pending_product.update(
                {
                    "product_name": context.get("product_name") or pending_product.get("product_name"),
                    "supplier_name": context.get("supplier_name") or pending_product.get("supplier_name"),
                    "lot_code": context.get("lot_code") or pending_product.get("lot_code"),
                }
            )
        state["pending_product"] = pending_product
        state["last_tool"] = "upsert_product"
        return state

    if route == "deterministic-sales-goal-clarification":
        state["pending_action"] = "sales_goal_write"
        state["pending_sales_goal"] = _merge_sales_goal_write_fragments(message, recent_conversation, state)
        state["last_tool"] = "write_sales_goal"
        return state

    if route == "deterministic-capability" and _is_inventory_consumption_estimation_request(normalized):
        purchase_year, inventory_year = _inventory_consumption_estimation_years(message, state)
        state["inventory_consumption_estimation_mode"] = "orders_minus_first_inventory_ignore_initial_stock"
        state["inventory_consumption_estimation_purchase_year"] = purchase_year
        state["inventory_consumption_estimation_inventory_year"] = inventory_year
        state["last_tool"] = "inventory_consumption_estimation"
        return state

    for tool_call in executed_tool_calls:
        arguments = tool_call.arguments if isinstance(tool_call.arguments, dict) else {}
        if tool_call.tool == "search_products":
            query = str(arguments.get("query") or "").strip()
            if query and not _is_generic_catalog_followup_query(query):
                state["catalog_query"] = query
            state["last_tool"] = "search_products"
        elif tool_call.tool in {"get_reservations_snapshot", "list_reservations"}:
            state["last_tool"] = tool_call.tool
            reservation_date = _coerce_optional_iso_date(arguments.get("target_date") or arguments.get("date"))
            if reservation_date is not None:
                state["reservation_date"] = reservation_date
            else:
                state.pop("reservation_date", None)
        elif tool_call.tool in {"get_purchase_overview", "get_purchase_frequency", "get_purchase_batches", "get_purchase_history"}:
            state["purchase_view"] = tool_call.tool
            state["last_tool"] = tool_call.tool
            state["purchase_query"] = str(arguments.get("query") or "").strip()
            year = _coerce_optional_year(arguments.get("year"))
            month = _coerce_optional_month(arguments.get("month"))
            start_date = _coerce_optional_iso_date(arguments.get("start_date"))
            end_date = _coerce_optional_iso_date(arguments.get("end_date"))
            if year is not None:
                state["purchase_year"] = year
            else:
                state.pop("purchase_year", None)
            if month is not None:
                state["purchase_month"] = month
            else:
                state.pop("purchase_month", None)
            if start_date is not None:
                state["purchase_start_date"] = start_date
            else:
                state.pop("purchase_start_date", None)
            if end_date is not None:
                state["purchase_end_date"] = end_date
            else:
                state.pop("purchase_end_date", None)
        elif tool_call.tool == "compare_purchase_periods":
            state["last_tool"] = "compare_purchase_periods"
            state["comparison"] = {
                "query": str(arguments.get("query") or "").strip(),
                "primary_year": _coerce_optional_year(arguments.get("primary_year")),
                "primary_month": _coerce_optional_month(arguments.get("primary_month")),
                "secondary_year": _coerce_optional_year(arguments.get("secondary_year")),
                "secondary_month": _coerce_optional_month(arguments.get("secondary_month")),
                "focus_hint": str(arguments.get("focus_hint") or "").strip() or _purchase_comparison_focus(normalized, str(arguments.get("query") or "")),
                "percentage_requested": bool(arguments.get("percentage_requested")),
            }
            state["purchase_query"] = str(arguments.get("query") or "").strip()
            state["purchase_year"] = _coerce_optional_year(arguments.get("primary_year"))
            state["purchase_month"] = _coerce_optional_month(arguments.get("primary_month"))
        elif tool_call.tool == "get_sales_goals":
            state["last_tool"] = "get_sales_goals"
            explicit_goal_year = _coerce_optional_year(arguments.get("year"))
            if explicit_goal_year is not None:
                state["sales_goals_year"] = explicit_goal_year
            else:
                state["sales_goals_year"] = _today_in_timezone().year
        elif tool_call.tool == "get_timeclock_summary":
            state["last_tool"] = "get_timeclock_summary"
            query_text = str(arguments.get("query_text") or message).strip()
            has_current_timeclock_subject = _is_timeclock_request(normalized)
            contextual_timeclock_followup = _is_timeclock_contextual_followup_request(message, normalized)
            if query_text and (has_current_timeclock_subject or not contextual_timeclock_followup or not state.get("timeclock_query_text")):
                state["timeclock_query_text"] = query_text
            scope = str(arguments.get("scope") or "today").strip()
            normalized_scope = scope if scope in {"today", "week", "all", "active"} else "today"
            has_argument_period = (
                normalized_scope in {"week", "all", "active"}
                or _coerce_optional_iso_date(arguments.get("target_date")) is not None
                or _coerce_optional_iso_date(arguments.get("start_date")) is not None
                or _coerce_optional_iso_date(arguments.get("end_date")) is not None
            )
            preserve_existing_period = (
                contextual_timeclock_followup
                and not _message_has_explicit_timeclock_period(message, normalized)
                and not has_argument_period
                and bool(state.get("timeclock_scope") or state.get("timeclock_start_date") or state.get("timeclock_end_date") or state.get("timeclock_target_date"))
            )
            if not preserve_existing_period:
                state["timeclock_scope"] = normalized_scope
                for source_key, state_key in (
                    ("target_date", "timeclock_target_date"),
                    ("start_date", "timeclock_start_date"),
                    ("end_date", "timeclock_end_date"),
                ):
                    value = _coerce_optional_iso_date(arguments.get(source_key))
                    if value is not None:
                        state[state_key] = value
                    else:
                        state.pop(state_key, None)
            if isinstance(arguments.get("include_entries"), bool):
                state["timeclock_include_entries"] = arguments["include_entries"]
        elif tool_call.tool == "run_tenant_query" and _sql_targets_supplier_catalog(str(arguments.get("sql") or "")):
            catalog_query = _extract_supplier_catalog_lookup_query_from_sql(str(arguments.get("sql") or ""))
            if not catalog_query:
                supplier_query = _extract_supplier_catalog_supplier_query(message)
                catalog_query = _extract_supplier_catalog_product_query(message, supplier_query)
            if catalog_query:
                state["catalog_query"] = catalog_query
            state["last_tool"] = "supplier_catalog_lookup"
        elif tool_call.tool == "run_tenant_query" and _sql_targets_homemade_stock(str(arguments.get("sql") or "")):
            for key in (
                "inventory_consumption_estimation_mode",
                "inventory_consumption_estimation_query",
                "inventory_consumption_estimation_purchase_year",
                "inventory_consumption_estimation_inventory_year",
            ):
                state.pop(key, None)
            state["last_tool"] = "homemade_stock_consumption"
        elif tool_call.tool == "run_tenant_query" and (
            _is_fiscal_spend_request(message, normalized) or _sql_is_fiscal_spend_query(str(arguments.get("sql") or ""))
        ):
            sql_text = str(arguments.get("sql") or "")
            fiscal_query = _extract_fiscal_spend_query(message) or _extract_sql_alias_literal(sql_text, "query")
            if fiscal_query and not _is_generic_purchase_followup_query(fiscal_query):
                state["fiscal_spend_query"] = fiscal_query
            period_label = _extract_sql_alias_literal(sql_text, "period")
            period_year = int(period_label) if re.fullmatch(r"20\d{2}", period_label) else None
            year = _extract_reference_year(message) or period_year or _coerce_optional_year(state.get("fiscal_spend_year"))
            month = _extract_reference_month(message) or _coerce_optional_month(state.get("fiscal_spend_month"))
            week_range = _extract_reference_week_range(message)
            if year is not None:
                state["fiscal_spend_year"] = year
            else:
                state.pop("fiscal_spend_year", None)
            if month is not None:
                state["fiscal_spend_month"] = month
            else:
                state.pop("fiscal_spend_month", None)
            if week_range is not None:
                state["fiscal_spend_start_date"] = week_range[0].isoformat()
                state["fiscal_spend_end_date"] = week_range[1].isoformat()
            elif " - " in period_label:
                start_date_label, end_date_label = period_label.split(" - ", 1)
                parsed_start = _coerce_optional_iso_date(start_date_label)
                parsed_end = _coerce_optional_iso_date(end_date_label)
                if parsed_start is not None and parsed_end is not None:
                    state["fiscal_spend_start_date"] = parsed_start
                    state["fiscal_spend_end_date"] = parsed_end
            elif _message_has_explicit_purchase_period(message):
                state.pop("fiscal_spend_start_date", None)
                state.pop("fiscal_spend_end_date", None)
            state["last_tool"] = "fiscal_spend_query"
        elif tool_call.tool == "run_tenant_query" and _is_inventory_consumption_estimation_request(normalized):
            query = _extract_inventory_consumption_estimation_query(message)
            purchase_year, inventory_year = _inventory_consumption_estimation_years(message, state)
            state["inventory_consumption_estimation_mode"] = "orders_minus_first_inventory_ignore_initial_stock"
            state["inventory_consumption_estimation_purchase_year"] = purchase_year
            state["inventory_consumption_estimation_inventory_year"] = inventory_year
            if query:
                state["inventory_consumption_estimation_query"] = query
            state["last_tool"] = "inventory_consumption_estimation"
        elif tool_call.tool == "upsert_product":
            state.pop("pending_action", None)
            state.pop("pending_product", None)
            state["last_tool"] = "upsert_product"
        elif tool_call.tool == "write_sales_goal":
            state.pop("pending_action", None)
            state.pop("pending_sales_goal", None)
            state["last_tool"] = "write_sales_goal"

    if not executed_tool_calls and tool_results:
        last_result = tool_results[-1]
        if isinstance(last_result, dict):
            tool_name = str(last_result.get("tool") or "").strip()
            if tool_name:
                state["last_tool"] = tool_name

    for tool_result in tool_results:
        if not isinstance(tool_result, dict):
            continue
        tool_name = str(tool_result.get("tool") or "").strip()
        result_payload = tool_result.get("result")
        if tool_name in {"get_purchase_overview", "get_purchase_frequency"} and isinstance(result_payload, dict):
            first_ordered_at = str(result_payload.get("first_ordered_at") or "").strip()
            last_ordered_at = str(result_payload.get("last_ordered_at") or "").strip()
            if first_ordered_at:
                state["purchase_first_ordered_at"] = first_ordered_at
            else:
                state.pop("purchase_first_ordered_at", None)
            if last_ordered_at:
                state["purchase_last_ordered_at"] = last_ordered_at
            else:
                state.pop("purchase_last_ordered_at", None)
        if tool_name == "get_timeclock_summary" and isinstance(result_payload, dict):
            resolved_user = result_payload.get("resolved_user") if isinstance(result_payload.get("resolved_user"), dict) else {}
            user_field_map = (
                ("user_id", "timeclock_resolved_user_id"),
                ("name", "timeclock_resolved_user_name"),
                ("username", "timeclock_resolved_username"),
                ("email", "timeclock_resolved_email"),
            )
            for source_key, state_key in user_field_map:
                value = resolved_user.get(source_key)
                if isinstance(value, str) and value.strip():
                    state[state_key] = value.strip()
                else:
                    state.pop(state_key, None)
    for tool_result in tool_results:
        if not isinstance(tool_result, dict) or str(tool_result.get("tool") or "").strip() != "upsert_product":
            continue
        result_payload = tool_result.get("result")
        if not isinstance(result_payload, dict):
            continue
        product_payload = result_payload.get("product") if isinstance(result_payload.get("product"), dict) else {}
        last_product_write = _normalize_last_product_write_state(
            {
                "status": result_payload.get("status"),
                "product_name": product_payload.get("product_name"),
                "lot_code": product_payload.get("lot_code"),
                "supplier_name": product_payload.get("supplier_name"),
            }
        )
        if last_product_write:
            state["last_product_write"] = last_product_write

    return state


def _extract_explicit_year(message: str) -> int | None:
    match = re.search(r"\b(20\d{2})\b", message)
    if not match:
        return None
    return int(match.group(1))


def _extract_reference_year(message: str) -> int | None:
    explicit = _extract_explicit_year(message)
    if explicit is not None:
        return explicit

    normalized = _normalize_text(message)
    current_year = _today_in_timezone().year
    month = _extract_reference_month(message)
    current_month = _today_in_timezone().month
    if month is not None and any(fragment in normalized for fragment in ("scorso", "scorsa", "passato", "passata")):
        return current_year if month < current_month else current_year - 1
    if month is not None and any(fragment in normalized for fragment in ("prossimo", "prossima")):
        return current_year if month > current_month else current_year + 1
    if "anno scorso" in normalized or "anno passato" in normalized:
        return current_year - 1
    if "quest anno" in normalized or "questo anno" in normalized:
        return current_year
    if "anno prossimo" in normalized:
        return current_year + 1
    return None


def _extract_reference_years(message: str) -> list[int]:
    explicit_matches = re.findall(r"\b(20\d{2})\b", message)
    explicit_years: list[int] = []
    for raw_year in explicit_matches:
        year_value = int(raw_year)
        if year_value not in explicit_years:
            explicit_years.append(year_value)
    if len(explicit_years) >= 2:
        return explicit_years[:2]

    normalized = _normalize_text(message)
    current_year = _today_in_timezone().year
    if "anno scorso" in normalized or "anno passato" in normalized:
        if current_year not in explicit_years:
            explicit_years.append(current_year)
        previous_year = current_year - 1
        if previous_year not in explicit_years:
            explicit_years.append(previous_year)
    if "quest anno" in normalized or "questo anno" in normalized:
        if current_year not in explicit_years:
            explicit_years.append(current_year)
    return explicit_years[:2]


def _extract_reference_month(message: str) -> int | None:
    normalized = _normalize_text(message)
    tokens = normalized.split()
    for token in tokens:
        month = _ITALIAN_MONTHS.get(token)
        if month is not None:
            return month
    return None


def _extract_reference_periods(message: str) -> list[tuple[int, int]]:
    normalized = _normalize_text(message)
    current_year = _today_in_timezone().year
    current_month = _today_in_timezone().month
    periods: list[tuple[int | None, int]] = []

    if any(fragment in normalized for fragment in ("questo mese", "mese corrente", "mese attuale")):
        periods.append((current_year, current_month))
    if any(fragment in normalized for fragment in ("mese scorso", "scorso mese", "mese passato", "passato mese")):
        year, month = _shift_year_month(current_year, current_month, -1)
        periods.append((year, month))
    if any(fragment in normalized for fragment in ("mese prossimo", "prossimo mese")):
        year, month = _shift_year_month(current_year, current_month, 1)
        periods.append((year, month))

    for match in _MONTH_REFERENCE_PATTERN.finditer(normalized):
        month_label = match.group(1)
        raw_year = match.group(2)
        month = _ITALIAN_MONTHS.get(month_label)
        if month is None:
            continue

        year: int | None = None
        if raw_year and re.fullmatch(r"20\d{2}", raw_year):
            year = int(raw_year)
        elif raw_year in {"scorso", "scorsa", "passato", "passata"}:
            year = current_year if month < current_month else current_year - 1
        elif raw_year in {"prossimo", "prossima"}:
            year = current_year if month > current_month else current_year + 1
        elif raw_year in {"corrente", "attuale"}:
            year = current_year

        periods.append((year, month))

    if not periods:
        return []

    explicit_years = _extract_reference_years(message)
    fallback_year = explicit_years[0] if len(explicit_years) == 1 else _extract_reference_year(message)
    if fallback_year is None:
        fallback_year = current_year

    resolved: list[tuple[int, int]] = []
    seen: set[tuple[int, int]] = set()
    for year, month in periods:
        key = ((year or fallback_year), month)
        if key in seen:
            continue
        seen.add(key)
        resolved.append(key)
    return resolved


def _extract_reference_week_of_month(message: str) -> int | None:
    normalized = _normalize_text(message)
    match = re.search(r"\b(prima|seconda|terza|quarta|quinta|ultima|1|2|3|4|5)\s+settiman\w*\b", normalized)
    if not match:
        return None
    token = match.group(1)
    if token == "prima" or token == "1":
        return 1
    if token == "seconda" or token == "2":
        return 2
    if token == "terza" or token == "3":
        return 3
    if token == "quarta" or token == "4":
        return 4
    if token == "quinta" or token == "5":
        return 5
    if token == "ultima":
        return -1
    return None


def _extract_reference_week_range(message: str) -> tuple[date, date] | None:
    week_index = _extract_reference_week_of_month(message)
    month = _extract_reference_month(message)
    if week_index is None or month is None:
        return None
    year = _extract_reference_year(message) or _today_in_timezone().year
    days_in_month = monthrange(year, month)[1]
    month_end_exclusive = date(year, month, days_in_month) + timedelta(days=1)
    if week_index == -1:
        start_day = max(days_in_month - 6, 1)
        start_value = date(year, month, start_day)
        return (start_value, month_end_exclusive)
    else:
        start_day = ((week_index - 1) * 7) + 1
        if start_day > days_in_month:
            return None
        start_value = date(year, month, start_day)
        return (start_value, min(start_value + timedelta(days=7), month_end_exclusive))


def _format_italian_month(month: int | None) -> str:
    if month is None:
        return ""
    for label, value in _ITALIAN_MONTHS.items():
        if value == month:
            return label
    return ""


def _format_purchase_period_label(year: object | None, month: object | None) -> str:
    month_value = month if isinstance(month, int) else None
    year_value = year if isinstance(year, int) else None
    month_label = _format_italian_month(month_value)
    if month_label and year_value:
        return f"{month_label} {year_value}"
    if year_value:
        return str(year_value)
    if month_label:
        return month_label
    return "questo periodo"


def _coerce_iso_date(value: object | None) -> date | None:
    if isinstance(value, date):
        return value
    if isinstance(value, str):
        try:
            return date.fromisoformat(value)
        except ValueError:
            return None
    return None


def _format_purchase_scope_label(
    *,
    start_date: object | None,
    end_date: object | None,
    year: object | None,
    month: object | None,
) -> str:
    start_value = _coerce_iso_date(start_date)
    end_exclusive = _coerce_iso_date(end_date)
    if start_value is not None and end_exclusive is not None:
        inclusive_end = end_exclusive - timedelta(days=1)
        return f"dal {start_value.strftime('%d/%m/%Y')} al {inclusive_end.strftime('%d/%m/%Y')}"
    period_label = _format_purchase_period_label(year, month)
    if period_label == "questo periodo":
        return ""
    if isinstance(month, int):
        return f"per {period_label}"
    if isinstance(year, int):
        return f"nel {period_label}"
    return ""


def _purchase_comparison_focus(normalized_message: str, query: str) -> Literal["products", "orders", "quantity", "amount"]:
    if _is_purchase_amount_request(normalized_message):
        return "amount"
    if any(fragment in normalized_message for fragment in ("merce", "roba")):
        return "quantity"
    if any(fragment in normalized_message for fragment in ("quantita", "pezzi", "litri", "litro", "volume", "volumi")):
        return "quantity"
    if "%" in normalized_message or "percentual" in normalized_message:
        if _contains_normalized_word(normalized_message, "ordine", "ordini", "acquisto", "acquisti"):
            return "orders"
        return "quantity"
    if _contains_normalized_word(normalized_message, "ordine", "ordini", "acquisto", "acquisti"):
        return "orders"
    if _contains_normalized_word(normalized_message, "prodotto", "prodotti", "articolo", "articoli"):
        return "products"
    return "quantity" if query.strip() else "products"


def _format_percentage(value: float) -> str:
    return f"{value:.1f}".replace(".", ",")


def _extract_product_query(message: str) -> str:
    tokens = [
        token
        for token in _tokenize_query(message)
        if token
        and not re.fullmatch(r"20\d{2}", token)
        and token not in _PRODUCT_QUERY_IGNORED_TOKENS
    ]
    return " ".join(tokens[:6]).strip()


def _extract_catalog_query(message: str) -> str:
    subject_query = _extract_catalog_subject_query(message).strip()
    if subject_query:
        return subject_query
    return _extract_product_query(message)


def _extract_inventory_query(message: str) -> str:
    tokens = [
        token
        for token in _tokenize_query(message)
        if token
        and not re.fullmatch(r"20\d{2}", token)
        and token not in _INVENTORY_QUERY_IGNORED_TOKENS
    ]
    return " ".join(tokens[:6]).strip()


def _extract_inventory_total_threshold(message: str) -> tuple[float, set[str]] | None:
    normalized = unicodedata.normalize("NFKD", message or "").encode("ascii", "ignore").decode("ascii")
    normalized = normalized.lower().replace("’", "'")
    searchable = re.sub(r"[^a-z0-9,.\s]+", " ", normalized)
    searchable = " ".join(searchable.split())
    patterns = (
        r"\b(?:meno(?:\s+(?:di|del|della|dello|delle|dei|degli))?|sotto(?:\s+(?:a|le|la|i|il))?|inferior[ei]?\s+(?:a|al|alla|alle|allo|ai|agli))\s+(\d+(?:[,.]\d+)?)\b",
        r"\b(\d+(?:[,.]\d+)?)\s+(?:o\s+)?(?:meno|inferior[ei]?)\b",
    )
    for pattern in patterns:
        match = re.search(pattern, searchable)
        if not match:
            continue
        raw_threshold = match.group(1)
        try:
            threshold = float(raw_threshold.replace(",", "."))
        except ValueError:
            continue
        if threshold <= 0:
            continue
        threshold_tokens = set(_normalize_text(raw_threshold).split())
        return threshold, threshold_tokens
    return None


def _is_inventory_ranking_request(normalized: str) -> bool:
    inventory_scope = (
        any(keyword in normalized for keyword in _INVENTORY_KEYWORDS)
        or " in casa" in f" {normalized} "
        or _contains_normalized_word(normalized, "unita", "quantita", "bottiglie", "cartoni")
    )
    if not inventory_scope:
        return False
    if _contains_normalized_word(normalized, "top", "classifica"):
        return True
    if _contains_normalized_word(normalized, "maggior", "maggiore", "maggiori", "massima", "massime", "massimi", "massimo"):
        return True
    if _contains_normalized_word(normalized, "piu") and _contains_normalized_word(
        normalized,
        "unita",
        "quantita",
        "giacenza",
        "giacenze",
        "rimanenza",
        "rimanenze",
        "scorta",
        "scorte",
        "stock",
        "bottiglie",
        "cartoni",
    ):
        return True
    return False


def _extract_inventory_rank_limit(message: str) -> int:
    normalized = _normalize_text(message)
    ranking_context = _is_inventory_ranking_request(normalized)
    for token in _tokenize_query(message):
        if not re.fullmatch(r"\d{1,4}", token):
            continue
        value = int(token)
        if 1 <= value <= _CHAT_LIST_LIMIT:
            return value
    return 20 if ranking_context else _CHAT_LIST_LIMIT


def _inventory_consumption_estimation_years(
    message: str,
    state: dict[str, object] | None = None,
) -> tuple[int, int]:
    normalized_state = _normalize_home_thread_state(state)
    explicit_years = _extract_reference_years(message)
    if len(explicit_years) >= 2:
        return explicit_years[0], explicit_years[1]
    if len(explicit_years) == 1:
        purchase_year = explicit_years[0]
        return purchase_year, purchase_year + 1

    state_purchase_year = _coerce_optional_year(normalized_state.get("inventory_consumption_estimation_purchase_year"))
    state_inventory_year = _coerce_optional_year(normalized_state.get("inventory_consumption_estimation_inventory_year"))
    if state_purchase_year is not None and state_inventory_year is not None:
        return state_purchase_year, state_inventory_year

    current_year = _today_in_timezone().year
    return current_year - 1, current_year


def _extract_inventory_consumption_estimation_query(message: str) -> str:
    ignored_tokens = _INVENTORY_QUERY_IGNORED_TOKENS | {
        "abbiamo",
        "acquisti",
        "acquisto",
        "calcola",
        "calcolami",
        "calcolato",
        "come",
        "conto",
        "consumare",
        "consumata",
        "consumate",
        "consumati",
        "consumato",
        "consumo",
        "consumi",
        "dati",
        "finale",
        "giornaliera",
        "giornaliere",
        "giornalieri",
        "giornaliero",
        "giorno",
        "giorni",
        "grado",
        "ignora",
        "ignorare",
        "ignoriamo",
        "iniziale",
        "iniziali",
        "inizio",
        "inventario",
        "inventari",
        "lascia",
        "lasciamo",
        "media",
        "medie",
        "medio",
        "medi",
        "allora",
        "attuale",
        "dimmi",
        "ok",
        "ma",
        "non",
        "ogni",
        "primo",
        "prodotti",
        "prodotto",
        "quotidiana",
        "quotidiane",
        "quotidiani",
        "quotidiano",
        "quelle",
        "rimanenze",
        "rimanenza",
        "sei",
        "stimare",
        "stimato",
        "stima",
        "storico",
        "stock",
        "tenendo",
        "tralascia",
        "tralasciamo",
    }
    tokens = [
        token
        for token in _tokenize_query(message)
        if token
        and not re.fullmatch(r"20\d{2}", token)
        and token not in ignored_tokens
        and not token.isdigit()
    ]
    return " ".join(tokens[:6]).strip()


def _build_inventory_consumption_estimation_tool_call(
    message: str,
    *,
    query_override: str | None = None,
    thread_state: dict[str, object] | None = None,
) -> PlannedToolCall:
    purchase_year, inventory_year = _inventory_consumption_estimation_years(message, thread_state)
    purchase_start = date(purchase_year, 1, 1)
    purchase_end_exclusive = date(purchase_year + 1, 1, 1)
    period_days = (purchase_end_exclusive - purchase_start).days
    query = (query_override or _extract_inventory_consumption_estimation_query(message)).strip()
    conditions: list[str] = []
    for token in _tokenize_query(query)[:6]:
        escaped_token = _escape_sql_literal(token)
        conditions.append(
            "("
            f"lower(product_name) LIKE '%{escaped_token}%' "
            f"OR lower(supplier_name) LIKE '%{escaped_token}%'"
            ")"
        )
    purchase_where = " AND ".join(conditions).replace("product_name", "items.product_name").replace("supplier_name", "items.supplier_name")
    inventory_where = " AND ".join(conditions).replace("product_name", "items.product_name").replace("supplier_name", "items.supplier_name")
    if purchase_where:
        purchase_where = f"AND {purchase_where} "
    if inventory_where:
        inventory_where = f"AND {inventory_where} "
    escaped_query = _escape_sql_literal(query)
    sql = (
        "WITH first_inventory_date AS ("
        "SELECT MIN(inventory_date) AS inventory_date "
        "FROM tenant_inventory_sessions "
        f"WHERE inventory_date >= '{inventory_year:04d}-01-01' AND inventory_date < '{inventory_year + 1:04d}-01-01'"
        "), "
        "purchase_totals AS ("
        "SELECT lower(items.product_name) AS product_lookup, lower(items.supplier_name) AS supplier_lookup, "
        "items.product_name AS product_name, items.supplier_name AS supplier_name, "
        "ROUND(SUM(items.quantity * COALESCE(NULLIF(items.units_per_pack, 0), 1)), 3) AS purchased_units "
        "FROM ordini_items AS items "
        "JOIN ordini_batches AS batches ON batches.id = items.batch_id "
        f"WHERE date(batches.confirmed_at) >= '{purchase_start.isoformat()}' "
        f"AND date(batches.confirmed_at) < '{purchase_end_exclusive.isoformat()}' "
        f"{purchase_where}"
        "GROUP BY product_lookup, supplier_lookup"
        "), "
        "inventory_totals AS ("
        "SELECT lower(items.product_name) AS product_lookup, lower(items.supplier_name) AS supplier_lookup, "
        "items.product_name AS product_name, items.supplier_name AS supplier_name, "
        "ROUND(SUM(items.total_equivalent_units), 3) AS final_stock_units "
        "FROM tenant_inventory_session_items AS items "
        "JOIN tenant_inventory_sessions AS sessions ON sessions.id = items.session_id "
        "JOIN first_inventory_date AS fid ON fid.inventory_date = sessions.inventory_date "
        f"WHERE 1 = 1 {inventory_where}"
        "GROUP BY product_lookup, supplier_lookup"
        "), "
        "all_products AS ("
        "SELECT product_lookup, supplier_lookup FROM purchase_totals "
        "UNION "
        "SELECT product_lookup, supplier_lookup FROM inventory_totals"
        ") "
        "SELECT "
        "1 AS consumption_estimate_result, "
        f"'{escaped_query}' AS query, "
        f"{purchase_year} AS purchase_year, "
        f"{inventory_year} AS inventory_year, "
        f"'{purchase_start.isoformat()}' AS purchase_start_date, "
        f"'{(purchase_end_exclusive - timedelta(days=1)).isoformat()}' AS purchase_end_date, "
        f"{period_days} AS period_days, "
        "fid.inventory_date AS final_inventory_date, "
        "COALESCE(p.product_name, i.product_name) AS product_name, "
        "COALESCE(p.supplier_name, i.supplier_name) AS supplier_name, "
        "ROUND(COALESCE(p.purchased_units, 0), 3) AS purchased_units, "
        "ROUND(COALESCE(i.final_stock_units, 0), 3) AS final_stock_units, "
        "ROUND(COALESCE(p.purchased_units, 0) - COALESCE(i.final_stock_units, 0), 3) AS estimated_consumed_units, "
        f"ROUND((COALESCE(p.purchased_units, 0) - COALESCE(i.final_stock_units, 0)) / {float(period_days):.1f}, 3) AS estimated_daily_units, "
        "'stock_iniziale_ignorato' AS estimation_method "
        "FROM all_products AS ap "
        "CROSS JOIN first_inventory_date AS fid "
        "LEFT JOIN purchase_totals AS p ON p.product_lookup = ap.product_lookup AND p.supplier_lookup = ap.supplier_lookup "
        "LEFT JOIN inventory_totals AS i ON i.product_lookup = ap.product_lookup AND i.supplier_lookup = ap.supplier_lookup "
        "WHERE COALESCE(p.purchased_units, 0) <> 0 OR COALESCE(i.final_stock_units, 0) <> 0 "
        "ORDER BY estimated_daily_units DESC, lower(COALESCE(p.supplier_name, i.supplier_name)) ASC, lower(COALESCE(p.product_name, i.product_name)) ASC "
        f"LIMIT {_CHAT_LIST_LIMIT}"
    )
    return PlannedToolCall(tool="run_tenant_query", arguments={"sql": sql, "limit": _CHAT_LIST_LIMIT})


def _format_sql_numeric_literal(value: float) -> str:
    rendered = f"{value:.6f}".rstrip("0").rstrip(".")
    return rendered or "0"


def _is_inventory_author_request(normalized: str) -> bool:
    if not any(keyword in normalized for keyword in ("inventario", "inventari")):
        return False
    if not _contains_normalized_word(normalized, "chi", "utente", "account", "persona", "dipendente", "staff"):
        return False
    return any(
        fragment in normalized
        for fragment in (
            "fatto",
            "fatta",
            "salvato",
            "salvata",
            "creato",
            "creata",
            "registrato",
            "registrata",
            "eseguito",
            "eseguita",
            "ultimo",
            "ultima",
        )
    )


def _build_inventory_author_tool_call(message: str) -> PlannedToolCall:
    normalized = _normalize_text(message)
    author_tokens = {
        "chi",
        "ha",
        "fatto",
        "fatta",
        "salvato",
        "salvata",
        "creato",
        "creata",
        "registrato",
        "registrata",
        "eseguito",
        "eseguita",
        "utente",
        "account",
        "persona",
        "dipendente",
        "staff",
        "ultimo",
        "ultima",
        "ultimi",
        "ultime",
    }
    warehouse_tokens = [
        token
        for token in _tokenize_query(message)
        if token
        and not (len(token) == 1 and token.isalpha())
        and not re.fullmatch(r"20\d{2}", token)
        and token not in _INVENTORY_QUERY_IGNORED_TOKENS
        and token not in author_tokens
    ]
    conditions = ["latest_inventory_session_id IS NOT NULL"]
    for token in warehouse_tokens[:6]:
        escaped_token = _escape_sql_literal(token)
        conditions.append(f"lower(warehouse_name) LIKE '%{escaped_token}%'")
    where_sql = " AND ".join(conditions)
    list_requested = (
        not warehouse_tokens
        and (
            _contains_normalized_word(normalized, "ultimi", "ultime", "tutti", "tutte", "ogni", "ciascun", "ciascuno")
            or "per magazzino" in normalized
            or "per ogni magazzino" in normalized
        )
    )
    result_limit = _CHAT_LIST_LIMIT if list_requested else 1
    sql = (
        "SELECT "
        "warehouse_name, "
        "latest_inventory_date, "
        "latest_inventory_created_by_name, "
        "latest_inventory_created_by_user_id, "
        "latest_inventory_created_at, "
        "latest_inventory_updated_at, "
        "latest_inventory_total_products, "
        "latest_inventory_total_equivalent_units, "
        "latest_inventory_session_id "
        "FROM inventory_warehouses "
        f"WHERE {where_sql} "
        "ORDER BY latest_inventory_created_at DESC, latest_inventory_date DESC, lower(warehouse_name) ASC "
        f"LIMIT {result_limit}"
    )
    return PlannedToolCall(tool="run_tenant_query", arguments={"sql": sql, "limit": result_limit})


def _latest_inventory_consumption_request_message(
    conversation: list[dict[str, str]],
) -> str:
    for item in reversed(conversation):
        if item.get("role") != "user":
            continue
        content = str(item.get("content") or "").strip()
        if content and _is_inventory_consumption_request(content, _normalize_text(content)):
            return content
    return ""


def _latest_homemade_stock_consumption_request_message(
    conversation: list[dict[str, str]],
) -> str:
    for item in reversed(conversation):
        if item.get("role") != "user":
            continue
        content = str(item.get("content") or "").strip()
        if content and _is_homemade_stock_consumption_request(_normalize_text(content)):
            return content
    return ""


def _extract_catalog_subject_query(message: str) -> str:
    normalized = _normalize_text(message)
    for pattern in (
        r"\b(?:su|del|della|dello|dei|delle|di)\s+(.+)$",
        r"\bha\s+(.+)$",
    ):
        match = re.search(pattern, normalized)
        if not match:
            continue
        tokens = [
            token
            for token in _tokenize_query(match.group(1))
            if token
            and not re.fullmatch(r"20\d{2}", token)
            and token not in _PRODUCT_QUERY_IGNORED_TOKENS
        ]
        candidate = " ".join(tokens[:8]).strip()
        if candidate:
            return candidate
    return ""


def _extract_purchase_supplier_query(message: str) -> str:
    normalized = _normalize_text(message)
    supplier_match = re.search(r"\b(?:da|dal|dalla|fornitore|fornitori)\b\s+(.+)$", normalized)
    if not supplier_match:
        return ""

    boundary_tokens = {
        "adesso",
        "agosto",
        "ai",
        "al",
        "alla",
        "alle",
        "aprile",
        "con",
        "corrente",
        "del",
        "della",
        "delle",
        "dei",
        "degli",
        "di",
        "domani",
        "e",
        "febbraio",
        "fra",
        "gennaio",
        "giugno",
        "ieri",
        "in",
        "luglio",
        "maggio",
        "marzo",
        "nel",
        "nella",
        "nelle",
        "oggi",
        "ottobre",
        "passata",
        "passate",
        "passati",
        "passato",
        "per",
        "prossima",
        "prossime",
        "prossimi",
        "prossimo",
        "quest",
        "questa",
        "queste",
        "questi",
        "questo",
        "scorsa",
        "scorse",
        "scorsi",
        "scorso",
        "settembre",
        "stasera",
        "tra",
    }

    supplier_tokens: list[str] = []
    for token in supplier_match.group(1).split():
        if re.fullmatch(r"20\d{2}", token):
            break
        if token in _ITALIAN_MONTHS or token in boundary_tokens:
            break
        if token in _PURCHASE_QUERY_IGNORED_TOKENS:
            if supplier_tokens:
                break
            continue
        supplier_tokens.append(token)

    return " ".join(supplier_tokens[:4]).strip()


def _extract_purchase_query(message: str) -> str:
    supplier_query = _extract_purchase_supplier_query(message)
    if supplier_query:
        return supplier_query

    normalized = _normalize_text(message)
    tokens = [
        token
        for token in _tokenize_query(message)
        if token
        and not re.fullmatch(r"20\d{2}", token)
        and token not in _PURCHASE_QUERY_IGNORED_TOKENS
    ]
    if _is_latest_batches_request(normalized):
        tokens = [token for token in tokens if not token.isdigit()]
    if _extract_chronological_order_rank(message) is not None:
        tokens = [token for token in tokens if not token.isdigit()]
    if _is_purchase_time_request(normalized):
        tokens = [token for token in tokens if not token.isdigit() and token not in {"ma", "quando", "quanto", "da"}]
    return " ".join(tokens[:6]).strip()


def _is_purchase_comparison_request(message: str, normalized: str) -> bool:
    periods = _extract_reference_periods(message)
    years = _extract_reference_years(message)
    has_multiple_periods = len(periods) >= 2 or len(years) >= 2
    if not has_multiple_periods:
        return False
    if any(fragment in normalized for fragment in ("confront", "rispetto", "vs", "versus")):
        return True
    if not any(keyword in normalized for keyword in ("ordin", "acquist", "compr", "prodotto", "prodotti")):
        return False
    if _contains_normalized_word(normalized, "piu", "meno", "maggiore", "maggior", "minore", "minori"):
        return True
    return bool(re.search(r"\b(?:tra|oppure|o)\b", normalized))


def _build_purchase_comparison_tool_call(
    message: str,
    *,
    fallback_year: int | None = None,
    fallback_query: str | None = None,
    focus_hint: Literal["products", "orders", "quantity", "amount"] | None = None,
    percentage_requested: bool | None = None,
) -> PlannedToolCall | None:
    periods = _extract_reference_periods(message)
    normalized = _normalize_text(message)
    if (
        fallback_year is not None
        and periods
        and not re.search(r"\b20\d{2}\b", normalized)
        and not any(token in normalized for token in ("scorso", "scorsa", "passato", "passata", "prossimo", "prossima", "corrente", "attuale"))
    ):
        periods = [(fallback_year, month) for _, month in periods]
    if len(periods) >= 2:
        query = _extract_purchase_query(message) or (fallback_query or "")
        return PlannedToolCall(
            tool="compare_purchase_periods",
            arguments={
                "query": query,
                "primary_year": periods[0][0],
                "primary_month": periods[0][1],
                "secondary_year": periods[1][0],
                "secondary_month": periods[1][1],
                "focus_hint": focus_hint,
                "percentage_requested": bool(percentage_requested),
                "limit": _CHAT_LIST_LIMIT,
            },
        )

    years = _extract_reference_years(message)
    month = _extract_reference_month(message)
    if len(years) >= 2:
        query = _extract_purchase_query(message) or (fallback_query or "")
        return PlannedToolCall(
            tool="compare_purchase_periods",
            arguments={
                "query": query,
                "primary_year": years[0],
                "primary_month": month,
                "secondary_year": years[1],
                "secondary_month": month,
                "focus_hint": focus_hint,
                "percentage_requested": bool(percentage_requested),
                "limit": _CHAT_LIST_LIMIT,
            },
        )

    if len(years) < 2:
        reference_year = _extract_reference_year(message)
        if reference_year is None:
            return None
        years = [reference_year, reference_year - 1]
    query = _extract_purchase_query(message) or (fallback_query or "")
    return PlannedToolCall(
        tool="compare_purchase_periods",
        arguments={
            "query": query,
            "primary_year": years[0],
            "primary_month": month,
            "secondary_year": years[1],
            "secondary_month": month,
            "focus_hint": focus_hint,
            "percentage_requested": bool(percentage_requested),
            "limit": _CHAT_LIST_LIMIT,
        },
    )


def _is_purchase_history_request(normalized: str) -> bool:
    return any(
        phrase in normalized
        for phrase in (
            "cosa ho ordinato",
            "che cosa ho ordinato",
            "cosa abbiamo ordinato",
            "che cosa abbiamo ordinato",
            "cosa ho acquistato",
            "che cosa ho acquistato",
            "cosa abbiamo acquistato",
            "che cosa abbiamo acquistato",
            "cosa ho comprato",
            "che cosa ho comprato",
            "cosa abbiamo comprato",
            "che cosa abbiamo comprato",
        )
    )


def _is_purchase_product_list_request(normalized: str) -> bool:
    if not any(fragment in normalized for fragment in ("lista", "elenco", "elenc", "quali", "mostra", "mostrami")):
        return False
    if not any(keyword in normalized for keyword in ("acquist", "compr", "ordin", "fornitor")):
        return False
    if " da " in f" {normalized} ":
        return True
    return _contains_normalized_word(normalized, "prodotto", "prodotti", "articolo", "articoli")


def _is_sql_analytics_request(normalized: str) -> bool:
    aggregate_markers = (
        "top",
        "primi",
        "classifica",
        "graduatoria",
        "numero di",
        "conteggio",
        "raggrupp",
        "group by",
    )
    if not any(marker in normalized for marker in aggregate_markers):
        return False
    analytic_dimensions = (
        "fornitor",
        "prodot",
        "catalog",
        "fattur",
        "document",
        "utent",
        "account",
        "prenot",
        "mance",
        "marca",
        "modul",
        "impostazion",
    )
    return any(dimension in normalized for dimension in analytic_dimensions)


def _build_sql_analytics_tool_calls(message: str, normalized: str) -> list[PlannedToolCall]:
    top_suppliers_match = re.search(r"\bprimi?\s+(\d+)\s+fornitor", normalized)
    if top_suppliers_match and "catalog" in normalized and "prodot" in normalized:
        try:
            limit = max(1, min(int(top_suppliers_match.group(1)), 20))
        except ValueError:
            limit = 3
        return [
            PlannedToolCall(
                tool="run_tenant_query",
                arguments={
                    "sql": (
                        "SELECT supplier_name, COUNT(*) AS total_products "
                        "FROM products "
                        "WHERE active = 1 "
                        "GROUP BY supplier_name "
                        "ORDER BY total_products DESC, supplier_name ASC "
                        f"LIMIT {limit}"
                    ),
                    "limit": limit,
                },
            )
        ]
    return []


def _extract_requested_quantity(message: str) -> int:
    lowered = message.lower()

    unit_words = r"(?:ct|cassa|casse|cartone|cartoni|bt|bottiglia|bottiglie)"
    if re.search(rf"\b(un|uno|una)\s+{unit_words}\b", lowered):
        return 1

    numeric_unit_match = re.search(rf"\b(\d+)\s*{unit_words}\b", lowered)
    if numeric_unit_match:
        return max(int(numeric_unit_match.group(1)), 1)

    generic_number_matches = re.finditer(r"\b(\d+)\b", lowered)
    for match in generic_number_matches:
        tail = lowered[match.end() : match.end() + 6]
        if re.match(r"\s*(?:cl|ml|l)\b", tail):
            continue
        return max(int(match.group(1)), 1)

    return 1


def _is_latest_batches_request(normalized: str) -> bool:
    return (
        _contains_normalized_word(normalized, "ordine", "ordini")
        and (
            _contains_normalized_word(normalized, "ultimo", "ultimi")
            or "penultim" in normalized
        )
    )


def _has_purchase_order_subject(normalized: str) -> bool:
    return (
        _contains_normalized_word(normalized, "ordine", "ordini")
        or "ordin" in normalized
        or "acquist" in normalized
        or "compr" in normalized
    )


def _extract_chronological_order_rank(message: str) -> int | None:
    normalized = _normalize_text(message)
    if not _has_purchase_order_subject(normalized):
        return None
    if _is_latest_batches_request(normalized):
        return None
    if "penultim" in normalized:
        return None

    numeric_match = re.search(
        r"\b([1-9]|[12]\d|30)(?:\s*°|\s*[oa])?\s+(?:ordine|ordini|acquisto|acquisti)\b",
        normalized,
    )
    if numeric_match:
        try:
            return max(1, min(int(numeric_match.group(1)), 30))
        except ValueError:
            pass

    for token in _tokenize_query(normalized):
        rank = _ITALIAN_ORDINAL_RANKS.get(token)
        if rank is not None:
            return rank

    first_time_ordered = "prima volta" in normalized and any(keyword in normalized for keyword in ("ordin", "acquist", "compr"))
    return 1 if first_time_ordered else None


def _is_first_batches_request(normalized: str) -> bool:
    first_order = _contains_normalized_word(normalized, "ordine", "ordini") and (
        _contains_normalized_word(normalized, "primo", "primi")
        or "piu vecchi" in normalized
        or "piu vecchio" in normalized
        or "più vecchi" in normalized
        or "più vecchio" in normalized
    )
    first_time_ordered = "prima volta" in normalized and any(keyword in normalized for keyword in ("ordin", "acquist", "compr"))
    return first_order or first_time_ordered


def _extract_requested_order_rank(message: str) -> int | None:
    normalized = _normalize_text(message)
    if "penultim" in normalized:
        return 2
    if _contains_normalized_word(normalized, "ultimo") and _contains_normalized_word(normalized, "ordine", "ordini"):
        return 1
    return _extract_chronological_order_rank(message)


def _extract_requested_latest_batches_limit(message: str) -> int:
    normalized = _normalize_text(message)
    if "penultim" in normalized:
        return 2

    numeric_match = re.search(r"\bultim\w*\s+(\d{1,3})\s+ordin\w*\b", normalized)
    if numeric_match:
        try:
            return max(1, min(int(numeric_match.group(1)), 30))
        except ValueError:
            pass

    word_match = re.search(
        r"\bultim\w*\s+(un|una|uno|due|tre|quattro|cinque|sei|sette|otto|nove|dieci)\s+ordin\w*\b",
        normalized,
    )
    if word_match:
        parsed = _parse_small_number_token(word_match.group(1))
        if parsed is not None:
            return max(1, min(parsed, 30))

    if _contains_normalized_word(normalized, "ultimi") and _contains_normalized_word(normalized, "ordini"):
        return 5
    return 1


def _is_purchase_time_request(normalized: str) -> bool:
    if any(fragment in normalized for fragment in ("da quanto", "da quando", "ma quando", "in che data", "ultima volta")):
        return True
    return "quando" in normalized and any(keyword in normalized for keyword in _ORDERS_KEYWORDS)


def _extract_purchase_batch_id(message: str) -> int | None:
    match = re.search(r"(?:ordine\s*#\s*|#\s*)(\d+)\b", message, re.IGNORECASE)
    if not match:
        return None
    try:
        return int(match.group(1))
    except ValueError:
        return None


def _extract_purchase_batch_date(message: str) -> date | None:
    match = re.search(r"\b(20\d{2}-\d{2}-\d{2})\b", message)
    if not match:
        return None
    try:
        return date.fromisoformat(match.group(1))
    except ValueError:
        return None


def _is_purchase_batch_detail_request(message: str, normalized: str) -> bool:
    if not any(keyword in normalized for keyword in ("mostra", "mostrami", "vedi", "vedere", "per esteso", "dettaglio", "dettagli", "apri")):
        return False
    if _extract_purchase_batch_id(message) is not None:
        return True
    if _contains_normalized_word(normalized, "ordine", "ordini") and _extract_purchase_batch_date(message) is not None:
        return True
    return False


def _extract_first_decimal_value(message: str, *anchors: str) -> float | None:
    if anchors:
        pattern = r"(?:%s)\s*[:=]?\s*(?:€\s*)?(\d+(?:[.,]\d+)?)" % "|".join(re.escape(anchor) for anchor in anchors)
        match = re.search(pattern, message, re.IGNORECASE)
        if match:
            try:
                return float(match.group(1).replace(",", "."))
            except ValueError:
                return None
    match = re.search(r"€\s*(\d+(?:[.,]\d+)?)", message, re.IGNORECASE)
    if not match:
        return None
    try:
        return float(match.group(1).replace(",", "."))
    except ValueError:
        return None


def _extract_note_text(message: str) -> str:
    colon_match = re.search(r":\s*(.+)$", message, re.DOTALL)
    if colon_match:
        return colon_match.group(1).strip()
    note_match = re.search(r"(?:nota(?: condivisa)?|note)\s+(.+)$", message, re.IGNORECASE)
    if note_match:
        return note_match.group(1).strip(" .,!?:;")
    return ""


def _contains_normalized_word(normalized: str, *words: str) -> bool:
    tokens = set(normalized.split())
    return any(word in tokens for word in words)


def _is_price_request(normalized: str) -> bool:
    return any(fragment in normalized for fragment in ("quanto costa", "che prezzo", "prezzo ha")) or _contains_normalized_word(
        normalized,
        "prezzo",
        "prezzi",
        "costa",
        "costano",
        "costare",
    )


def _is_lowest_price_request(normalized: str) -> bool:
    return any(
        fragment in normalized
        for fragment in (
            "costa meno",
            "costa di meno",
            "meno caro",
            "meno cara",
            "meno costoso",
            "meno costosa",
            "piu economico",
            "piu economica",
        )
    )


def _is_price_per_weight_request(normalized: str) -> bool:
    return any(
        fragment in normalized
        for fragment in (
            "prezzo al chilo",
            "prezzo al kg",
            "prezzo per chilo",
            "prezzo per kg",
            "euro al chilo",
            "euro al kg",
            "prezzo kilo",
        )
    ) or (
        "prezzo" in normalized
        and any(token in normalized for token in ("chilo", "kilo", "kg"))
    )


def _is_missing_catalog_price_request(normalized: str) -> bool:
    if "senza prezzo" in normalized or "prezzo non disponibile" in normalized:
        return True
    if "prezzo" not in normalized and "prezzi" not in normalized:
        return False
    return any(
        fragment in normalized
        for fragment in (
            "non hai il prezzo",
            "non ha il prezzo",
            "non hanno il prezzo",
            "non hai prezzi",
            "non ha prezzi",
            "non hanno prezzi",
            "non hai il prezzi",
            "manca il prezzo",
            "mancano i prezzi",
            "mancanti di prezzo",
            "mancante di prezzo",
        )
    )


def _is_catalog_data_request(normalized: str) -> bool:
    return any(
        fragment in normalized
        for fragment in (
            "tutti i dati",
            "tutti i dettagli",
            "mostrami i dati",
            "mostra i dati",
            "dati che abbiamo",
            "che dati abbiamo",
            "fammi vedere i dati",
            "scheda prodotto",
            "scheda completa",
            "informazioni su",
            "info su",
            "mostrami tutto su",
            "mostra tutto su",
        )
    )


def _is_units_per_pack_request(normalized: str) -> bool:
    if any(
        fragment in normalized
        for fragment in (
            "unit per pack",
            "units per pack",
            "quante unita contiene",
            "quante bottiglie contiene",
            "da quante bottiglie",
            "pack size",
        )
    ):
        return True
    return "pack" in normalized and any(token in normalized for token in ("quante", "quanto", "unit", "units", "bottiglie"))


def _format_eur(value: float | int | None) -> str | None:
    if value is None:
        return None
    rendered = f"{float(value):.2f}".replace(".", ",")
    return f"€ {rendered}"


def _format_compact_number(value: float | int | None) -> str | None:
    if value is None:
        return None
    numeric = float(value)
    if numeric.is_integer():
        return str(int(numeric))
    return f"{numeric:.2f}".rstrip("0").rstrip(".").replace(".", ",")


def _parse_decimal_fragment(value: str) -> float | None:
    cleaned = (value or "").strip().replace(",", ".")
    if not cleaned:
        return None
    try:
        numeric = float(cleaned)
    except ValueError:
        return None
    return numeric if numeric > 0 else None


def _infer_weight_range_kg(product_name: str, lot_code: str | None = None) -> tuple[float | None, float | None]:
    searchable = _normalize_text(" ".join(part for part in (product_name, lot_code or "") if part))
    pack_patterns = (
        (r"\bkg\s*(\d+(?:\.\d+)?)\s*x\s*(\d+(?:\.\d+)?)(?:\s*pz)?\b", 1.0),
        (r"\b(\d+(?:\.\d+)?)\s*x\s*(\d+(?:\.\d+)?)\s*kg\b", 1.0),
        (r"\bg\s*(\d+(?:\.\d+)?)\s*x\s*(\d+(?:\.\d+)?)(?:\s*pz)?\b", 1000.0),
        (r"\b(\d+(?:\.\d+)?)\s*x\s*(\d+(?:\.\d+)?)\s*g\b", 1000.0),
    )
    for pattern, divisor in pack_patterns:
        match = re.search(pattern, searchable, re.IGNORECASE)
        if not match:
            continue
        unit_weight = _parse_decimal_fragment(match.group(1))
        pack_count = _parse_decimal_fragment(match.group(2))
        if unit_weight is None or pack_count is None:
            continue
        total_weight = round((unit_weight * pack_count) / divisor, 4)
        return total_weight, total_weight
    patterns = (
        (r"(\d+(?:\.\d+)?)\s*/\s*(\d+(?:\.\d+)?)\s*kg\b", 1.0),
        (r"(\d+(?:\.\d+)?)\s*/\s*(\d+(?:\.\d+)?)\s*g\b", 1000.0),
        (r"\bkg[\.\s-]*(\d+(?:\.\d+)?)\b", 1.0),
        (r"(\d+(?:\.\d+)?)\s*kg\b", 1.0),
        (r"(\d+(?:\.\d+)?)\s*g\b", 1000.0),
    )
    for pattern, divisor in patterns:
        match = re.search(pattern, searchable, re.IGNORECASE)
        if not match:
            continue
        first = _parse_decimal_fragment(match.group(1))
        if first is None:
            continue
        second = _parse_decimal_fragment(match.group(2)) if match.lastindex and match.lastindex >= 2 else None
        if second is None:
            weight = round(first / divisor, 4)
            return weight, weight
        low = round(min(first, second) / divisor, 4)
        high = round(max(first, second) / divisor, 4)
        return low, high
    return None, None


def _catalog_item_price_per_kg(item: dict[str, object]) -> tuple[float | None, float | None]:
    explicit_price_per_kg = _coerce_positive_float(item.get("unit_price_per_kg"))
    if explicit_price_per_kg is not None:
        explicit_price_per_kg = round(explicit_price_per_kg, 2)
        return explicit_price_per_kg, explicit_price_per_kg
    price = _coerce_positive_float(item.get("final_price_vat"))
    if price is None:
        return None, None
    explicit_weight = _coerce_positive_float(item.get("weight_kg"))
    if explicit_weight is not None:
        explicit_price = round(price / explicit_weight, 2)
        return explicit_price, explicit_price
    low_weight, high_weight = _infer_weight_range_kg(
        str(item.get("product_name") or ""),
        str(item.get("lot_code") or ""),
    )
    if low_weight is None or high_weight is None:
        return None, None
    low_price = round(price / high_weight, 2)
    high_price = round(price / low_weight, 2)
    return low_price, high_price


def _is_historical_purchase_request(message: str, normalized: str) -> bool:
    if _extract_reference_year(message) is not None:
        return True

    if any(fragment in normalized for fragment in ("quante volte", "quanti ordini", "ultimo ordine", "ultimi ordini")):
        return True

    if _contains_normalized_word(
        normalized,
        "storico",
        "quando",
        "ordine",
        "ordini",
        "ultimo",
        "ultimi",
        "effettuato",
        "effettuati",
        "ordinato",
        "ordinati",
    ):
        return True

    return False


def _is_reservation_write_request(normalized: str) -> bool:
    return "prenot" in normalized and _contains_normalized_word(
        normalized,
        "crea",
        "prenota",
        "inserisci",
        "aggiungi",
        "modifica",
        "sposta",
        "cambia",
        "annulla",
        "cancella",
        "elimina",
    )


def _contains_goal_keyword(normalized: str) -> bool:
    return "obiettiv" in normalized or "obbiettiv" in normalized or "goal" in normalized or "target" in normalized


def _is_sales_goal_write_request(normalized: str) -> bool:
    return _contains_goal_keyword(normalized) and _contains_normalized_word(
        normalized,
        "crea",
        "imposta",
        "aggiungi",
        "aggiorna",
        "modifica",
        "cambia",
        "fissa",
        "fissare",
        "definisci",
        "definire",
        "setta",
        "settare",
        "elimina",
        "cancella",
    )


def _is_sales_goal_read_request(normalized: str) -> bool:
    return _contains_goal_keyword(normalized) and any(
        keyword in normalized
        for keyword in (
            "mostra",
            "mostrami",
            "lista",
            "elenca",
            "vedere",
            "leggi",
            "quali",
            "abbiamo",
            "come siamo messi",
            "come va",
            "come vanno",
            "situazione",
            "stato",
            "andamento",
            "progress",
            "progresso",
            "manca",
            "mancano",
            "completare",
            "completato",
            "completati",
            "annuale",
            "annuali",
            "grafico",
            "chart",
        )
    )


def _is_sales_goal_graph_request(normalized: str) -> bool:
    return any(
        fragment in normalized
        for fragment in (
            "grafico",
            "grafica",
            "chart",
            "barre",
            "grafico a barre",
        )
    )


def _is_note_write_request(normalized: str) -> bool:
    return _contains_normalized_word(normalized, "nota", "note") and any(
        keyword in normalized for keyword in ("aggiungi", "scrivi", "salva", "crea", "aggiorna", "modifica", "elimina", "cancella")
    )


def _is_suspended_order_write_request(message: str, normalized: str) -> bool:
    return bool(_SUSPENDED_ORDER_PATTERN.search(message)) and any(
        keyword in normalized for keyword in ("crea", "aggiungi", "aggiungere", "metti", "inserisci", "imposta")
    )


def _is_write_intent(message: str, normalized: str) -> bool:
    return any(
        (
            _is_reservation_write_request(normalized),
            _is_product_write_request(message, normalized),
            _is_sales_goal_write_request(normalized),
            _is_note_write_request(normalized),
            _is_suspended_order_write_request(message, normalized),
            _is_document_create_request(normalized),
        )
    )


def _is_state_change_write_intent(message: str, normalized: str) -> bool:
    return any(
        (
            _is_reservation_write_request(normalized),
            _is_product_write_request(message, normalized),
            _is_sales_goal_write_request(normalized),
            _is_note_write_request(normalized),
            _is_suspended_order_write_request(message, normalized),
        )
    )


def _is_tenant_user_list_request(normalized: str) -> bool:
    return (
        any(keyword in normalized for keyword in ("account", "accounts", "utente", "utenti", "username", "staff", "dipendent"))
        and any(keyword in normalized for keyword in ("quali", "lista", "elenco", "mostra", "mostrami", "vedere", "leggi", "genera", "crea", "ci sono", "ha"))
        and not any(keyword in normalized for keyword in _TIMECLOCK_KEYWORDS)
    )


def _extract_module_settings_target(normalized: str) -> Literal["ordini", "prenotazioni", "whatsapp", "fiscal", "llm"] | None:
    if "whatsapp" in normalized:
        return "whatsapp"
    if "prenot" in normalized:
        return "prenotazioni"
    if "fattur" in normalized or "fiscal" in normalized or "documenti fisc" in normalized or "bolla" in normalized or "bolle" in normalized:
        return "fiscal"
    if _contains_normalized_word(normalized, "llm", "modello", "ai"):
        return "llm"
    if _contains_normalized_word(normalized, "ordine", "ordini"):
        return "ordini"
    return None


def _is_module_settings_read_request(normalized: str) -> bool:
    module = _extract_module_settings_target(normalized)
    if module is None:
        return False
    return any(keyword in normalized for keyword in ("impostaz", "configuraz", "config", "attive", "attivo", "stato"))


def _is_grounded_data_request(message: str, normalized: str) -> bool:
    if _is_write_intent(message, normalized):
        return True
    if _is_fiscal_spend_request(message, normalized):
        return True
    if _is_homemade_stock_consumption_request(normalized):
        return True
    if _is_inventory_consumption_estimation_request(normalized):
        return True
    if _is_timeclock_request(normalized):
        return True
    if _is_tips_request(message, normalized):
        return True
    if _is_inventory_request(message, normalized):
        return True
    if any(keyword in normalized for keyword in _RESERVATION_KEYWORDS):
        return True
    if any(keyword in normalized for keyword in _ORDERS_KEYWORDS):
        return True
    if _is_units_per_pack_request(normalized):
        return True
    if any(fragment in normalized for fragment in ("prezzo", "quanto costa", "che prezzo", "catalogo", "tra i miei prodotti")):
        return True
    if _contains_normalized_word(normalized, "nota", "note") or _contains_goal_keyword(normalized):
        return True
    if _is_tenant_user_list_request(normalized) or _is_module_settings_read_request(normalized):
        return True
    return False


def _is_catalog_request(message: str, normalized: str, query: str) -> bool:
    if not query.strip():
        return False
    if _contains_goal_keyword(normalized):
        return False

    if _is_catalog_data_request(normalized):
        return True

    if _is_units_per_pack_request(normalized):
        return True

    if _is_price_request(normalized):
        return True

    if _contains_normalized_word(normalized, "prodotto", "prodotti", "catalogo", "cataloghi", "articolo", "articoli"):
        return True

    if any(fragment in normalized for fragment in ("ci sono", "cosa abbiamo", "che abbiamo", "tra i miei prodotti")):
        return True

    if any(keyword in normalized for keyword in ("compriamo", "acquistiamo", "teniamo", "usiamo")):
        return not _is_historical_purchase_request(message, normalized)

    return False


def _is_supplier_catalog_request(normalized: str) -> bool:
    if "catalog" in normalized and "fornitor" in normalized:
        return True
    normalized_tokens = set(_tokenize_query(normalized))
    if normalized_tokens & _KNOWN_SUPPLIER_CATALOG_QUERY_TOKENS and _contains_normalized_word(
        normalized, "vende", "vendono", "vendere", "tratta", "trattano"
    ):
        return True
    if re.search(
        r"\b(?:ha|hanno|vende|vendono|tratta|trattano)\s+[a-z0-9][a-z0-9 '&.-]{1,60}\s+"
        r"(?:nel|nello|nella|sul|sulla|in)\s+(?:suo|sua|proprio|propria)?\s*catalog[oi]\b",
        normalized,
    ):
        return True
    if re.search(
        r"\b[a-z0-9][a-z0-9 '&.-]{1,60}\s+(?:ha|hanno|vende|vendono|tratta|trattano)\s+"
        r".{1,80}\b(?:in|nel|nello|nella|sul|sulla)\s+catalog[oi]\b",
        normalized,
    ):
        return True
    if re.search(r"\bcatalog[oi]\s+(?:di\s+|del\s+|della\s+|fornitore\s+|venditore\s+)?[a-z0-9]", normalized):
        if not any(fragment in normalized for fragment in ("catalogo prodotti del locale", "catalogo locale")):
            return True
    if _contains_normalized_word(normalized, "vende", "vendono", "vendere") and _contains_normalized_word(
        normalized, "fornitore", "fornitori"
    ):
        return True
    return any(
        fragment in normalized
        for fragment in (
            "chi vende",
            "chi ha in vendita",
            "quale fornitore ha",
            "quali fornitori hanno",
            "fornitore ha",
            "fornitori hanno",
        )
    )


def _extract_supplier_catalog_supplier_query(message: str) -> str:
    normalized = _normalize_text(message)
    known_supplier_patterns = (
        r"\b([a-z0-9][a-z0-9 '&.-]{1,40})\s+(?:vende|vendono|tratta|trattano)\s+[a-z0-9]",
        r"\b(?:che|quale|quali|cosa)?\s*[a-z0-9][a-z0-9 '&.-]{1,80}\s+"
        r"(?:vende|vendono|tratta|trattano)\s+([a-z0-9][a-z0-9 '&.-]{1,40})\b",
    )
    for supplier_pattern in known_supplier_patterns:
        supplier_match = re.search(supplier_pattern, normalized)
        if not supplier_match:
            continue
        candidate_tokens = [
            token
            for token in _tokenize_query(supplier_match.group(1))
            if token and token not in _PRODUCT_QUERY_IGNORED_TOKENS
        ]
        known_tokens = [token for token in candidate_tokens if token in _KNOWN_SUPPLIER_CATALOG_QUERY_TOKENS]
        if known_tokens:
            return " ".join(known_tokens[:3]).strip()

    supplier_patterns = (
        r"\b(?:ha|hanno|vende|vendono|tratta|trattano)\s+([a-z0-9][a-z0-9 '&.-]{1,60})\s+"
        r"(?:nel|nello|nella|sul|sulla|in)\s+(?:suo|sua|proprio|propria)?\s*catalog[oi]\b",
        r"\b([a-z0-9][a-z0-9 '&.-]{1,60})\s+(?:ha|hanno|vende|vendono|tratta|trattano)\s+"
        r".{1,80}\b(?:in|nel|nello|nella|sul|sulla)\s+catalog[oi]\b",
    )
    for supplier_pattern in supplier_patterns:
        supplier_match = re.search(supplier_pattern, normalized)
        if supplier_match:
            tokens = [
                token
                for token in _tokenize_query(supplier_match.group(1))
                if token
                and token not in _PRODUCT_QUERY_IGNORED_TOKENS
                and token
                not in {
                    "catalogo",
                    "cataloghi",
                    "prodotti",
                    "prodotto",
                    "locale",
                    "fornitore",
                    "fornitori",
                    "venditore",
                    "venditori",
                    "suo",
                    "sua",
                    "proprio",
                    "propria",
                }
            ]
            if tokens:
                return " ".join(tokens[:4]).strip()

    match = re.search(
        r"\bcatalog[oi]\s+(?:prodotti\s+)?(?:di\s+|del\s+|della\s+|fornitore\s+|venditore\s+)?([a-z0-9][a-z0-9 '&.-]{1,80})$",
        normalized,
    )
    if not match:
        return ""
    tokens = [
        token
        for token in _tokenize_query(match.group(1))
        if token
        and token not in _PRODUCT_QUERY_IGNORED_TOKENS
        and token not in {"catalogo", "cataloghi", "prodotti", "prodotto", "locale", "fornitore", "fornitori", "venditore", "venditori"}
    ]
    return " ".join(tokens[:4]).strip()


def _extract_supplier_catalog_product_query(message: str, supplier_query: str) -> str:
    normalized = _normalize_text(message)
    if supplier_query:
        supplier_pattern = re.escape(supplier_query).replace(r"\ ", r"\s+")
        normalized = re.sub(
            rf"\b(?:nel|nello|nella|sul|sulla|del|della|di)?\s*catalog[oi]\s+(?:prodotti\s+)?(?:di\s+|del\s+|della\s+|fornitore\s+|venditore\s+)?{supplier_pattern}\b",
            " ",
            normalized,
        )
    tokens = [
        token
        for token in _tokenize_query(normalized)
        if token
        and token not in _PRODUCT_QUERY_IGNORED_TOKENS
        and token
        not in {
            "catalogo",
            "cataloghi",
            "prodotti",
            "prodotto",
            "locale",
            "fornitore",
            "fornitori",
            "venditore",
            "venditori",
            "quanto",
            "quanta",
            "quanti",
            "quante",
            "costa",
            "costano",
            "prezzo",
            "prezzi",
            "quale",
            "quali",
            "nostro",
            "nostri",
            "vende",
            "vendono",
            "vendere",
            "ivato",
            "ivati",
            "suo",
            "sua",
            "proprio",
            "propria",
        }
    ]
    if supplier_query:
        supplier_tokens = set(_tokenize_query(supplier_query))
        tokens = [token for token in tokens if token not in supplier_tokens]
    return " ".join(tokens[:8]).strip()


def _is_catalog_search_followup_request(normalized: str) -> bool:
    return any(
        fragment in normalized
        for fragment in (
            "cerca nei prodotti",
            "cerca nel catalogo",
            "cerca tra i prodotti",
            "guarda nei prodotti",
            "guarda nel catalogo",
        )
    )


def _extract_catalog_subject_from_assistant_message(content: str) -> str:
    pack_context = _extract_pack_size_context_from_assistant_message(content)
    if pack_context and pack_context.get("product_name"):
        return _extract_product_query(str(pack_context.get("product_name") or ""))

    for pattern in (
        r"catalogo prodotti del locale\s+(.+?)\s+risulta venduto da",
        r"cataloghi fornitori caricati\s+(.+?)\s+risulta venduto da",
        r"articoli che corrispondono a\s+(.+?)(?:\s*:|$)",
        r"articoli riconducibili alla famiglia\s+(.+?)(?:\s*:|$)",
        r"varianti rilevanti per\s+(.+?)(?:\s*:|$)",
        r"dati che trovo su\s+(.+?)(?:\s*:|$)",
        r"Ho aggiunto il prodotto\s+(.+?)(?:\s+\(|\.)",
        r"Ho aggiornato il prodotto\s+(.+?)(?:\s+\(|\.)",
    ):
        match = re.search(pattern, content, re.IGNORECASE)
        if match:
            query = _extract_product_query(match.group(1))
            if query and not _is_generic_catalog_followup_query(query):
                return query
    return ""


def _latest_catalog_subject_query(conversation: list[dict[str, str]]) -> str:
    for item in reversed(conversation[-8:]):
        role = str(item.get("role") or "")
        content = str(item.get("content") or "")
        if not content:
            continue
        if role == "user":
            normalized = _normalize_text(content)
            if (
                _is_supplier_catalog_request(normalized)
                or _is_catalog_data_request(normalized)
                or _is_units_per_pack_request(normalized)
                or _is_product_write_request(content, normalized)
                or _is_catalog_request(content, normalized, _extract_product_query(content))
            ):
                supplier_query = _extract_supplier_catalog_supplier_query(content)
                query = _extract_supplier_catalog_product_query(content, supplier_query) or _extract_catalog_subject_query(content)
                if query and not _is_generic_catalog_followup_query(query):
                    return query
        elif role == "assistant":
            query = _extract_catalog_subject_from_assistant_message(content)
            if query and not _is_generic_catalog_followup_query(query):
                return query
    return ""


def _extract_purchase_overview_subject_from_assistant_message(content: str) -> str:
    normalized = _normalize_text(content)
    for pattern in (
        r"prodotti acquistati da\s+([a-z0-9\s&'\.-]+?)(?:\s*:|$)",
        r"prodotti acquistati che corrispondono a\s+([a-z0-9\s&'\.-]+?)(?:\s*:|$)",
    ):
        match = re.search(pattern, normalized, re.IGNORECASE)
        if not match:
            continue
        query = _extract_purchase_query(match.group(1))
        if query:
            return query
    return ""


def _latest_purchase_overview_query(conversation: list[dict[str, str]]) -> str:
    for item in reversed(conversation[-8:]):
        if str(item.get("role") or "") != "user":
            continue
        content = str(item.get("content") or "")
        if not content:
            continue
        normalized = _normalize_text(content)
        if _is_purchase_product_list_request(normalized):
            query = _extract_purchase_query(content)
            if query:
                return query

    for item in reversed(conversation[-8:]):
        role = str(item.get("role") or "")
        content = str(item.get("content") or "")
        if not content:
            continue
        if role == "user":
            continue
        elif role == "assistant":
            query = _extract_purchase_overview_subject_from_assistant_message(content)
            if query:
                return query
    return ""


def _latest_purchase_subject_query(conversation: list[dict[str, str]]) -> str:
    for item in reversed(conversation[-8:]):
        if str(item.get("role") or "") != "user":
            continue
        content = str(item.get("content") or "")
        if not content:
            continue
        normalized = _normalize_text(content)
        if not (
            any(keyword in normalized for keyword in _ORDERS_KEYWORDS)
            or _is_purchase_amount_request(normalized)
            or _is_liters_request(normalized)
            or _is_purchase_history_request(normalized)
            or _is_purchase_time_request(normalized)
            or _is_purchase_product_list_request(normalized)
        ):
            continue
        query = _extract_purchase_query(content)
        if query and not _is_generic_purchase_followup_query(query):
            return query
    return ""


def _focus_catalog_items(items: list[dict[str, object]], query: str) -> tuple[list[dict[str, object]], bool]:
    if not query.strip():
        return items, False

    if len(_catalog_query_tokens(query)) < 2:
        return items, False

    query_tokens = _significant_catalog_query_tokens(query)
    if not query_tokens:
        return items, False

    exact_matches: list[dict[str, object]] = []
    for item in items:
        searchable = " ".join(
            filter(
                None,
                [
                    str(item.get("product_name") or ""),
                    str(item.get("supplier_name") or ""),
                    str(item.get("lot_code") or ""),
                    str(item.get("product_code") or ""),
                    str(item.get("category") or ""),
                ],
            )
        )
        if _searchable_matches_all_query_tokens(searchable, query_tokens):
            exact_matches.append(item)

    if exact_matches:
        exact_matches.sort(
            key=lambda item: (
                str(item.get("product_name") or "").lower(),
                str(item.get("supplier_name") or "").lower(),
                str(item.get("lot_code") or "").lower(),
            )
        )
        return exact_matches, len(exact_matches) != len(items)

    return items, False


def _extract_product_lot_code_for_write(message: str) -> str | None:
    match = re.search(r"\b(?:lotto|lot|formato)\s+([A-Za-z0-9%./xX+\-]+)\b", message, re.IGNORECASE)
    if not match:
        return None
    return match.group(1).strip()


def _extract_product_supplier_for_write(message: str) -> str | None:
    match = re.search(r"\bfornitore\s+([A-Za-zÀ-ÿ0-9' .&-]{2,80}?)(?=\s+(?:prezzo|iva|categoria|note|lotto|lot|formato)\b|[,.]|$)", message, re.IGNORECASE)
    if not match:
        return None
    return re.sub(r"\s+", " ", match.group(1)).strip(" .,-")


def _extract_product_code_for_write(message: str) -> str | None:
    match = re.search(r"\b(?:codice|sku)\s+([A-Za-z0-9./_-]{2,60})\b", message, re.IGNORECASE)
    if not match:
        return None
    return match.group(1).strip()


def _extract_product_category_for_write(message: str) -> str | None:
    match = re.search(r"\bcategoria\s+([A-Za-zÀ-ÿ0-9' .&/_-]{2,80}?)(?=\s+(?:prezzo|iva|note|lotto|lot|formato|fornitore)\b|[,.]|$)", message, re.IGNORECASE)
    if not match:
        return None
    return re.sub(r"\s+", " ", match.group(1)).strip(" .,-")


def _extract_product_units_per_pack_for_write(message: str) -> float | None:
    patterns = (
        r"\b(?:units?\s*per\s*pack|unit(?:a|à|a')\s+per\s+(?:cartone|cassa|confezione))\s*[:=]?\s*(\d+(?:[.,]\d+)?)\b",
        r"\b(?:cartone|cassa|confezione)\s+da\s+(\d+(?:[.,]\d+)?)\b",
        r"\b(\d+(?:[.,]\d+)?)\s+(?:bottiglie|pezzi|unit(?:a|à|a'))\s+per\s+(?:cartone|cassa|confezione)\b",
        r"\bci\s+sono\s+(\d+(?:[.,]\d+)?)\s+(?:bottiglie|pezzi|unit(?:a|à|a'))\s+in\s+un\s+(?:cartone|cassa|confezione)\b",
        r"\bsono\s+(\d+(?:[.,]\d+)?)\s+(?:bottiglie|pezzi|unit(?:a|à|a'))\s+in\s+un\s+(?:cartone|cassa|confezione)\b",
    )
    for pattern in patterns:
        match = re.search(pattern, message, re.IGNORECASE)
        if not match:
            continue
        try:
            return float(match.group(1).replace(",", "."))
        except ValueError:
            continue

    stripped = message.strip()
    if re.fullmatch(r"\d{1,3}(?:[.,]\d+)?", stripped):
        try:
            return float(stripped.replace(",", "."))
        except ValueError:
            return None
    return None


def _extract_product_unit_price_per_kg_for_write(message: str) -> float | None:
    patterns = (
        r"(\d+(?:[.,]\d+)?)\s+(?:come\s+)?prezzo\s+(?:al|per)\s+(?:kg|chilo|kilo)\b",
        r"\bprezzo\s+(?:al|per)\s+(?:kg|chilo|kilo)\s*[:=]?\s*(\d+(?:[.,]\d+)?)\b",
        r"(\d+(?:[.,]\d+)?)\s*(?:€|euro)?\s*(?:\/\s*(?:kg|chilo|kilo)|al\s+(?:kg|chilo|kilo))\b",
    )
    for pattern in patterns:
        match = re.search(pattern, message, re.IGNORECASE)
        if not match:
            continue
        try:
            return float(match.group(1).replace(",", "."))
        except ValueError:
            continue
    return None


def _extract_product_vat_rate_for_write(message: str) -> float | None:
    return _extract_first_decimal_value(message, "iva")


def _extract_product_notes_for_write(message: str) -> str | None:
    match = re.search(r"\bnote?\s+(.+)$", message, re.IGNORECASE)
    if not match:
        return None
    candidate = re.sub(r"\s+", " ", match.group(1)).strip(" .,-")
    return candidate or None


def _extract_explicit_product_name_for_write(message: str) -> str | None:
    explicit = re.search(
        r"\bprodotto\s+([A-Za-zÀ-ÿ0-9°%/' .,&()\-]+?)(?=\s+(?:lotto|lot|formato|fornitore|prezzo|iva|categoria|note)\b|[,.]|$)",
        message,
        re.IGNORECASE,
    )
    if explicit:
        candidate = re.sub(r"\s+", " ", explicit.group(1)).strip(" .,-")
        return candidate or None
    return None


def _extract_product_subject_for_unit_price_per_kg_write(message: str) -> str | None:
    patterns = (
        r"\bprezzo\s+(?:al|per)\s+(?:kg|chilo|kilo)\s+(?:per|di)\s+(?:il|lo|la|l')\s*([A-Za-zÀ-ÿ0-9°%/' .,&()\-]+)$",
        r"\b(?:\/\s*(?:kg|chilo|kilo)|al\s+(?:kg|chilo|kilo))\s+(?:per|di)\s+(?:il|lo|la|l')\s*([A-Za-zÀ-ÿ0-9°%/' .,&()\-]+)$",
    )
    for pattern in patterns:
        match = re.search(pattern, message, re.IGNORECASE)
        if not match:
            continue
        candidate = re.sub(r"\s+", " ", match.group(1)).strip(" .,-")
        if candidate:
            return candidate
    return None


def _extract_product_name_for_write(message: str) -> str | None:
    explicit_name = _extract_explicit_product_name_for_write(message)
    if explicit_name:
        return explicit_name

    cleaned = message
    cleaned = re.sub(
        r"\b(?:aggiungi|aggiungere|crea|creare|inserisci|inserire|registra|registrare|appunta|appuntare|aggiorna|aggiornare|modifica|modificare|imposta|impostare|metti|mettere|cancella|cancellare|elimina|eliminare|rimuovi|rimuovere)\b",
        " ",
        cleaned,
        flags=re.IGNORECASE,
    )
    cleaned = re.sub(r"\b(?:no|un|una|uno|nuovo|nuova|questo|questa|questi|queste)\b", " ", cleaned, flags=re.IGNORECASE)
    cleaned = re.sub(r"\bprodott[oi]\b", " ", cleaned, flags=re.IGNORECASE)
    cleaned = re.sub(
        r"\b(?:come\s+)?prezzo\s+(?:al|per)\s+(?:kg|chilo|kilo)\s+(?:per|di)\s+(?:il|lo|la|l')\s*",
        " ",
        cleaned,
        flags=re.IGNORECASE,
    )
    cleaned = re.sub(r"\b(?:lotto|lot|formato)\s+[A-Za-z0-9%./xX+\-]+\b", " ", cleaned, flags=re.IGNORECASE)
    cleaned = re.sub(r"\bfornitore\s+[A-Za-zÀ-ÿ0-9' .&-]{2,80}(?=\s+(?:prezzo|iva|categoria|note|lotto|lot|formato)\b|[,.]|$)", " ", cleaned, flags=re.IGNORECASE)
    cleaned = re.sub(r"\b(?:codice|sku)\s+[A-Za-z0-9./_-]{2,60}\b", " ", cleaned, flags=re.IGNORECASE)
    cleaned = re.sub(r"\bprezzo\s*[:=]?\s*(?:€\s*)?\d+(?:[.,]\d+)?\b", " ", cleaned, flags=re.IGNORECASE)
    cleaned = re.sub(r"\b\d+(?:[.,]\d+)?\s+(?:come\s+)?prezzo\s+(?:al|per)\s+(?:kg|chilo|kilo)\b", " ", cleaned, flags=re.IGNORECASE)
    cleaned = re.sub(r"\biva\s*[:=]?\s*\d+(?:[.,]\d+)?%?\b", " ", cleaned, flags=re.IGNORECASE)
    cleaned = re.sub(r"€\s*\d+(?:[.,]\d+)?", " ", cleaned)
    cleaned = re.sub(r"\b(?:per|di)\s+(?:il|lo|la|l')\s+", " ", cleaned, flags=re.IGNORECASE)
    cleaned = re.sub(r"[?]", " ", cleaned)
    cleaned = re.sub(r"\s+", " ", cleaned).strip(" .,-")
    if not cleaned:
        return None
    if not re.search(r"[A-Za-zÀ-ÿ]", cleaned):
        return None

    normalized_cleaned = _normalize_text(cleaned)
    if not normalized_cleaned:
        return None
    normalized_tokens = [token for token in normalized_cleaned.split() if token not in _STOPWORDS]
    if not normalized_tokens or set(normalized_tokens).issubset(_PRODUCT_WRITE_NAME_NOISE):
        return None
    return cleaned


def _is_product_write_request(message: str, normalized: str) -> bool:
    if _is_document_create_request(normalized):
        return False
    if _is_product_capability_question(message, normalized):
        return False
    action_requested = any(
        keyword in normalized
        for keyword in ("aggiung", "crea", "inserisc", "registr", "aggiorn", "modific", "impost", "mett", "cancell", "elimin", "rimuov")
    )
    if not action_requested:
        return False
    if "prodot" in normalized:
        return True
    if _extract_explicit_product_name_for_write(message):
        return True
    product_name = _extract_product_name_for_write(message)
    field_requested = any(
        keyword in normalized
        for keyword in ("prezzo", "iva", "lotto", "lot", "fornitore", "categoria", "note", "cartone", "cassa", "kg", "chilo", "kilo", "pack")
    )
    return bool(product_name and field_requested)


def _build_product_write_tool_call(message: str) -> PlannedToolCall:
    return PlannedToolCall(tool="upsert_product", arguments=_parse_product_write_fragment(message))


def _extract_product_write_operation(message: str) -> Literal["upsert", "delete"]:
    normalized = _normalize_text(message)
    if any(keyword in normalized for keyword in ("cancella", "cancellare", "elimina", "eliminare", "rimuovi", "rimuovere")):
        return "delete"
    return "upsert"


def _parse_product_write_fragment(message: str) -> dict[str, object]:
    arguments: dict[str, object] = {"operation": _extract_product_write_operation(message)}
    lot_code = _extract_product_lot_code_for_write(message)
    supplier_name = _extract_product_supplier_for_write(message)
    product_code = _extract_product_code_for_write(message)
    category = _extract_product_category_for_write(message)
    units_per_pack = _extract_product_units_per_pack_for_write(message)
    unit_price_per_kg = _extract_product_unit_price_per_kg_for_write(message)
    price = _extract_first_decimal_value(message, "prezzo")
    vat_rate = _extract_product_vat_rate_for_write(message)
    notes = _extract_product_notes_for_write(message)
    normalized = _normalize_text(message)

    product_name = _extract_explicit_product_name_for_write(message)
    if not product_name and unit_price_per_kg is not None:
        product_name = _extract_product_subject_for_unit_price_per_kg_write(message)
    if arguments["operation"] == "delete" and not product_name:
        product_name = None
    elif not product_name and not any(
        (
            lot_code,
            supplier_name,
            product_code,
            category,
            price is not None,
            unit_price_per_kg is not None,
            vat_rate is not None,
            notes,
        )
    ):
        product_name = _extract_product_name_for_write(message)
    elif not product_name and _is_product_write_request(message, normalized):
        product_name = _extract_product_name_for_write(message)

    if product_name:
        arguments["product_name"] = product_name
    if lot_code:
        arguments["lot_code"] = lot_code
    if supplier_name:
        arguments["supplier_name"] = supplier_name
    if product_code:
        arguments["product_code"] = product_code
    if category:
        arguments["category"] = category
    if unit_price_per_kg is not None:
        arguments["unit_price_per_kg"] = unit_price_per_kg
    elif price is not None:
        arguments["final_price_vat"] = price
    if vat_rate is not None:
        arguments["vat_rate"] = vat_rate
    if units_per_pack is not None:
        arguments["units_per_pack"] = units_per_pack
    if notes:
        arguments["notes"] = notes
    return arguments


def _message_allows_missing_units_per_pack(message: str) -> bool:
    normalized = _normalize_text(message)
    return any(
        fragment in normalized
        for fragment in (
            "salva comunque",
            "salvalo comunque",
            "salvala comunque",
            "completo dopo",
            "aggiungo dopo",
            "lo aggiungo dopo",
            "la aggiungo dopo",
            "non lo so",
            "non la so",
            "non lo conosco",
        )
    )


def _extract_pack_size_context_from_assistant_message(content: str) -> dict[str, str] | None:
    liters_goal_match = re.search(
        r"cartone\/cassa\s+di\s+questi\s+prodotti\s*:\s*(.+?)\.\s*(?:Aggiorna|Dimmi|Se)",
        content,
        re.IGNORECASE,
    )
    if liters_goal_match:
        entries = re.findall(r"([^,()]+?)\s*\(([^,()]+),\s*([^)]+)\)", liters_goal_match.group(1))
        if len(entries) == 1:
            product_name = re.sub(r"\s+", " ", entries[0][0]).strip(" .,-")
            lot_code = re.sub(r"\s+", " ", entries[0][1]).strip(" .,-")
            supplier_name = re.sub(r"\s+", " ", entries[0][2]).strip(" .,-")
            if product_name:
                return {
                    "product_name": product_name,
                    "supplier_name": supplier_name,
                    "lot_code": lot_code,
                    "mode": "missing",
                }

    missing_match = re.search(
        r"Per\s+(.+?)(?:\s+\(([^,()]+),\s*([^)]+)\))?\s+il dato unita per pack non e ancora salvato\.",
        content,
        re.IGNORECASE,
    )
    if missing_match:
        product_name = re.sub(r"\s+", " ", missing_match.group(1)).strip(" .,-")
        supplier_name = re.sub(r"\s+", " ", (missing_match.group(2) or "")).strip(" .,-")
        lot_code = re.sub(r"\s+", " ", (missing_match.group(3) or "")).strip(" .,-")
        if product_name:
            return {
                "product_name": product_name,
                "supplier_name": supplier_name,
                "lot_code": lot_code,
                "mode": "missing",
            }

    no_need_match = re.search(
        r"Per\s+(.+?)(?:\s+\(([^,()]+),\s*([^)]+)\))?\s+non serve un valore unita per pack: il lotto e\s+([A-Za-z0-9._/\-]+)",
        content,
        re.IGNORECASE,
    )
    if no_need_match:
        product_name = re.sub(r"\s+", " ", no_need_match.group(1)).strip(" .,-")
        supplier_name = re.sub(r"\s+", " ", (no_need_match.group(2) or "")).strip(" .,-")
        lot_code = re.sub(r"\s+", " ", (no_need_match.group(4) or no_need_match.group(3) or "")).strip(" .,-")
        if product_name:
            return {
                "product_name": product_name,
                "supplier_name": supplier_name,
                "lot_code": lot_code,
                "mode": "not_needed",
            }
    return None


def _latest_pack_size_context(conversation: list[dict[str, str]]) -> dict[str, str] | None:
    for item in reversed(conversation[-6:]):
        if item.get("role") != "assistant":
            continue
        context = _extract_pack_size_context_from_assistant_message(str(item.get("content") or ""))
        if context is not None:
            return context
    return None


def _latest_pack_size_context_from_state(thread_state: dict[str, object] | None) -> dict[str, str] | None:
    state = _normalize_home_thread_state(thread_state)
    pending_action = str(state.get("pending_action") or "").strip()
    pending_product = state.get("pending_product") if isinstance(state.get("pending_product"), dict) else {}
    if pending_action != "product_write" or not pending_product:
        return None
    product_name = str(pending_product.get("product_name") or "").strip()
    lot_code = str(pending_product.get("lot_code") or "").strip()
    supplier_name = str(pending_product.get("supplier_name") or "").strip()
    if not product_name or not lot_code:
        return None
    if not _lot_requires_units_per_pack(lot_code):
        return None
    units_per_pack = _coerce_positive_float(pending_product.get("units_per_pack"))
    if units_per_pack is not None:
        return None
    return {
        "product_name": product_name,
        "supplier_name": supplier_name,
        "lot_code": lot_code,
        "mode": "missing",
    }


def _is_pack_size_update_intent(normalized: str) -> bool:
    return any(
        keyword in normalized
        for keyword in ("inserisc", "aggiung", "salva", "registr", "imposta", "metti", "voglio inserir", "voglio salvar", "ci sono", "sono")
    )


def _extract_sales_goal_name_for_write(message: str) -> str | None:
    block_name = re.search(
        r"\b(?:nuovo\s+)?obb?iettivo\s*:\s*([^\n,.;]{2,120})",
        message,
        re.IGNORECASE,
    )
    if block_name:
        candidate = re.sub(r"\s+", " ", block_name.group(1)).strip(" .,-")
        if candidate:
            return candidate

    explicit = re.search(
        r"\b(?:nome(?:\s+dell'?obb?iettivo)?|obb?iettivo\s+nome|goal\s+nome)\s*:?\s*([A-Za-zÀ-ÿ0-9'\" .,&()/_-]{2,120}?)(?=\s+(?:fornitore|prodotto|target|obb?iettiv|anno|tipo|litri?|quantita|pezzi)\b|[,.]|$)",
        message,
        re.IGNORECASE,
    )
    if not explicit:
        return None
    candidate = re.sub(r"\s+", " ", explicit.group(1)).strip(" .,-")
    return candidate or None


def _extract_sales_goal_supplier_for_write(message: str) -> str | None:
    explicit_matches = list(
        re.finditer(
            r"\bfornitore\s+([A-Za-zÀ-ÿ0-9' .&/_-]{2,80}?)(?=\s*(?:,|;|\.|\)|$)|\s+(?:il|la|i|gli|le|obiettiv|target|prodott|anno|tipo|solo|litri?|quantita|pezzi|unita|qualsiasi|tutti)\b)",
            message,
            re.IGNORECASE,
        )
    )
    if explicit_matches:
        candidate = re.sub(r"\s+", " ", explicit_matches[-1].group(1)).strip(" .,-")
        if candidate:
            return candidate

    alias_match = re.search(
        r"([A-Za-zÀ-ÿ0-9' .&/_-]{2,80}?)\s*\(\s*come\s+fornitore\s*\)",
        message,
        re.IGNORECASE,
    )
    if alias_match:
        candidate = re.sub(r"\s+", " ", alias_match.group(1)).strip(" .,-")
        if candidate:
            return candidate
    return None


def _extract_sales_goal_product_for_write(message: str) -> str | None:
    normalized = _normalize_text(message)
    if "fornitore" in normalized and any(fragment in normalized for fragment in ("tutti quelli", "qualsiasi suo prodotto", "tutti i prodotti")):
        return None

    grouped_products = re.search(
        r"\b(?:somma|insieme|totale)\s+di\s+questi\s+prodotti\s*:\s*(.+)$",
        message,
        re.IGNORECASE,
    ) or re.search(
        r"\bprodotti\s*:\s*(.+)$",
        message,
        re.IGNORECASE,
    )
    if grouped_products:
        raw_candidates = re.split(r"\s*,\s*|\s*;\s*", grouped_products.group(1))
        cleaned_candidates: list[str] = []
        for raw_candidate in raw_candidates:
            candidate = re.sub(r"\s+", " ", raw_candidate).strip(" .,-")
            if not candidate:
                continue
            if candidate.casefold() in {"e", "ed", "oppure", "o"}:
                continue
            cleaned_candidates.append(candidate)
        unique_candidates: list[str] = []
        seen_candidates: set[str] = set()
        for candidate in cleaned_candidates:
            marker = candidate.casefold()
            if marker in seen_candidates:
                continue
            seen_candidates.add(marker)
            unique_candidates.append(candidate)
        if unique_candidates:
            return unique_candidates[0] if len(unique_candidates) == 1 else " | ".join(unique_candidates)

    explicit_patterns = (
        r"\b(?:il\s+)?prodotto\s+si\s+chiama\s*:?\s*([A-Za-zÀ-ÿ0-9'\" .,&()/_-]{2,120}?)(?=\s+(?:fornitore|target|obb?iettiv|anno|tipo|litri?|quantita|pezzi)\b|[,;]|$)",
        r"\bprodotto\s*:?\s*([A-Za-zÀ-ÿ0-9'\" .,&()/_-]{2,120}?)(?=\s+(?:fornitore|target|obb?iettiv|anno|tipo|litri?|quantita|pezzi)\b|[,;]|$)",
        r"\bsi\s+chiama\s*:?\s*([A-Za-zÀ-ÿ0-9'\" .,&()/_-]{2,120}?)(?=\s+(?:fornitore|target|obb?iettiv|anno|tipo|litri?|quantita|pezzi)\b|[,;]|$)",
    )
    for pattern in explicit_patterns:
        explicit = re.search(pattern, message, re.IGNORECASE)
        if not explicit:
            continue
        candidate = re.sub(r"\s+", " ", explicit.group(1)).strip(" .,-")
        if candidate:
            return candidate

    goal_lead = re.search(
        r"\b(?:obb?iettiv\w*|goal)\s*:?\s*([A-Za-zÀ-ÿ0-9'\" .,&()/_-]{2,80}?)(?=\s*,?\s*\d+(?:[.,]\d+)?\s+(?:bottiglie|bottiglia|pezzi|pezzo|articoli|articolo|unita|litri?|litro)\b)",
        message,
        re.IGNORECASE,
    )
    if goal_lead:
        candidate = re.sub(r"\s+", " ", goal_lead.group(1)).strip(" .,-")
        if candidate:
            return candidate

    loose_lead = re.match(
        r"^\s*([A-Za-zÀ-ÿ0-9'\" .,&()/_-]{2,80}?)(?=\s*,\s*\d+(?:[.,]\d+)?\s+(?:bottiglie|bottiglia|pezzi|pezzo|articoli|articolo|unita)\b)",
        message,
        re.IGNORECASE,
    )
    if loose_lead:
        candidate = re.sub(r"\s+", " ", loose_lead.group(1)).strip(" .,-")
        return candidate or None
    return None


def _is_sales_goal_product_identification_message(message: str) -> bool:
    normalized = _normalize_text(message)
    if "prodotto" in normalized and "si chiama" in normalized:
        return True
    if bool(re.search(r"\bprodotto\s*:\s*[A-Za-zÀ-ÿ0-9]", message, re.IGNORECASE)):
        return True
    return bool(re.search(r"^\s*si\s+chiama\s*:?\s*[A-Za-zÀ-ÿ0-9]", message, re.IGNORECASE))


def _extract_sales_goal_target_for_write(message: str) -> float | None:
    if _is_sales_goal_product_identification_message(message):
        return None

    anchored = re.search(r"\b(?:obb?iettiv\w*|target|goal)\b(?:\s+(?:di|da|a|su))?\s*(\d+(?:[.,]\d+)?)", message, re.IGNORECASE)
    if anchored:
        try:
            return float(anchored.group(1).replace(",", "."))
        except ValueError:
            return None

    unit_matches = re.finditer(
        r"(\d+(?:[.,]\d+)?)\s*(bottiglie|bottiglia|pezzi|pezzo|articoli|articolo|unita|litri|litro|l)\b",
        message,
        re.IGNORECASE,
    )
    for match in unit_matches:
        raw_value = match.group(1)
        unit = str(match.group(2) or "").lower()
        if unit == "l" and not re.search(r"\b(?:target|obb?iettiv\w*|goal|fornitore|prodotto)\b", _normalize_text(message)):
            continue
        try:
            return float(raw_value.replace(",", "."))
        except ValueError:
            continue

    for match in re.finditer(r"\d+(?:[.,]\d+)?", message):
        raw_value = match.group(0)
        if re.fullmatch(r"20\d{2}", raw_value):
            continue
        trailing = message[match.end() :]
        if re.match(r"\s*(?:l|lt|cl|ml|kg|g)\b", trailing, re.IGNORECASE):
            continue
        try:
            return float(raw_value.replace(",", "."))
        except ValueError:
            continue
    return None


def _extract_sales_goal_type_for_write(message: str) -> Literal["quantity", "liters", "liters_dual", "note"] | None:
    normalized = _normalize_text(message)
    if "liters dual" in normalized or "litri dual" in normalized:
        return "liters_dual"
    if _contains_normalized_word(normalized, "nota", "note"):
        return "note"
    if any(token in normalized for token in ("quantita", "pezzi", "pezzo", "articoli", "articolo", "unita", "bottiglia", "bottiglie")):
        return "quantity"
    if _is_sales_goal_product_identification_message(message):
        return None
    if any(token in normalized for token in ("litri", "litro")) or re.search(r"\b\d+(?:[.,]\d+)?\s*l\b", normalized):
        return "liters"
    return None


def _extract_sales_goal_operation(message: str) -> Literal["upsert", "delete"] | None:
    normalized = _normalize_text(message)
    if any(token in normalized for token in ("elimina", "cancella", "rimuovi")):
        return "delete"
    if any(token in normalized for token in ("aggiungi", "crea", "imposta", "modifica", "aggiorna", "cambia", "fissa", "fissare", "definisci", "definire", "setta", "settare")):
        return "upsert"
    return None


def _extract_sales_goal_description_for_write(message: str) -> str | None:
    if re.search(r"^\s*(?:nome(?:\s+dell'?obb?iettivo)?|obb?iettivo\s+nome|goal\s+nome)\s*:?", message, re.IGNORECASE):
        return None

    explicit = re.search(r"\b(?:descrizione|nota(?:\s+dell'?obiettivo)?)\s*:?\s*(.+)$", message, re.IGNORECASE)
    if explicit:
        candidate = re.sub(r"\s+", " ", explicit.group(1)).strip(" .,-")
        return candidate or None

    block_description = re.search(
        r"\b(?:devo\s+)?(?:creare|aggiungere|impostare|fissare|definire|aggiornare|modificare)\s+(?:un\s+|una\s+)?(?:nuov[oa]\s+)?obb?iettivo\s*:\s*[^\n]+[\r\n]+(.+)$",
        message,
        re.IGNORECASE | re.DOTALL,
    )
    if block_description:
        candidate = re.sub(r"\s+", " ", block_description.group(1)).strip(" .,-")
        return candidate or None

    normalized = _normalize_text(message)
    if any(fragment in normalized for fragment in ("solo una nota", "e solo una nota", "e una nota", "è una nota")):
        return None
    if not any(keyword in normalized for keyword in ("obbiettiv", "obiettiv", "goal", "target")):
        return None

    candidate = re.sub(
        r"^\s*(?:devo\s+)?(?:aggiungere|aggiungi|crea|imposta|fissa|setta|definisci|aggiorna|modifica)\s+",
        "",
        message,
        flags=re.IGNORECASE,
    )
    candidate = re.sub(r"^\s*(?:come\s+)?(?:un\s+|una\s+)?obb?iettiv\w*\s*:?\s*", "", candidate, flags=re.IGNORECASE)
    candidate = re.sub(r"^\s*(?:come\s+)?goal\s*:?\s*", "", candidate, flags=re.IGNORECASE)
    candidate = re.sub(r"^\s*(?:che\s+)?", "", candidate, flags=re.IGNORECASE)
    candidate = re.sub(r"\s+", " ", candidate).strip(" .,-")
    if not candidate:
        return None
    if re.fullmatch(r"(?:solo\s+)?(?:una\s+)?nota", candidate, re.IGNORECASE):
        return None
    return candidate


def _split_goal_match_terms(value: str | None) -> list[str]:
    cleaned = _clean_optional_text(value)
    if not cleaned:
        return []
    parts = re.split(r"\s*\|\s*|\s*,\s*|\s*;\s*", cleaned)
    terms: list[str] = []
    seen: set[str] = set()
    for part in parts:
        candidate = re.sub(r"\s+", " ", part).strip(" .,-")
        if not candidate:
            continue
        marker = candidate.casefold()
        if marker in seen:
            continue
        seen.add(marker)
        terms.append(candidate)
    return terms


def _build_default_sales_goal_name(arguments: dict[str, object]) -> str | None:
    supplier_match = _clean_optional_text(arguments.get("supplier_match") if isinstance(arguments.get("supplier_match"), str) else None)
    product_match = _clean_optional_text(arguments.get("product_match") if isinstance(arguments.get("product_match"), str) else None)
    description = _clean_optional_text(arguments.get("description") if isinstance(arguments.get("description"), str) else None)
    target_year = arguments.get("year")
    if not isinstance(target_year, int):
        target_year = _today_in_timezone().year

    label = supplier_match or product_match
    if not label:
        if not description:
            return None
        shortened = description[:64].strip()
        if len(description) > 64:
            shortened = shortened.rstrip() + "..."
        label = shortened
    return f"Target {label} {target_year}"


def _parse_sales_goal_write_fragment(message: str) -> dict[str, object]:
    arguments: dict[str, object] = {}
    operation = _extract_sales_goal_operation(message)
    if operation is not None:
        arguments["operation"] = operation

    goal_name = _extract_sales_goal_name_for_write(message)
    supplier_match = _extract_sales_goal_supplier_for_write(message)
    product_match = _extract_sales_goal_product_for_write(message)
    goal_type = _extract_sales_goal_type_for_write(message)
    target = _extract_sales_goal_target_for_write(message)
    description = _extract_sales_goal_description_for_write(message)
    year = _extract_reference_year(message)

    if goal_name:
        arguments["name"] = goal_name
    if supplier_match:
        arguments["supplier_match"] = supplier_match
    if product_match:
        arguments["product_match"] = product_match
    if description:
        arguments["description"] = description
    if goal_type is not None:
        arguments["goal_type"] = goal_type
        if goal_type == "liters":
            arguments["unit_label"] = "L"
        elif goal_type == "quantity":
            normalized = _normalize_text(message)
            if "bottigli" in normalized:
                arguments["unit_label"] = "bottiglie"
            else:
                arguments["unit_label"] = "articoli"
    if target is not None:
        arguments["target"] = target
    if year is not None:
        arguments["year"] = year

    if (
        arguments.get("operation") != "delete"
        and "name" not in arguments
        and any(key in arguments for key in ("supplier_match", "product_match", "target", "description"))
    ):
        default_name = _build_default_sales_goal_name(arguments)
        if default_name:
            arguments["name"] = default_name
    return arguments


def _is_product_capability_question(message: str, normalized: str) -> bool:
    if "prodot" not in normalized:
        return False
    if not any(keyword in normalized for keyword in ("aggiung", "crea", "inserisc", "registr")):
        return False
    lowered = message.casefold()
    if "?" in message:
        return True
    return any(
        fragment in lowered
        for fragment in (
            "posso ",
            "puoi ",
            "si puo",
            "si può",
            "si possono",
            "potete ",
            "possiamo ",
            "come faccio ad aggiungere",
            "come aggiungo",
        )
    )


def _is_sales_data_request_unavailable(normalized: str) -> bool:
    if "obiettiv" in normalized:
        return False
    return any(
        keyword in normalized
        for keyword in ("venduto", "vendite", "incasso", "incassi", "fatturato", "scontrin", "corrispettiv", "cassa")
    )


def _extract_document_kind(normalized: str) -> Literal["doc", "sheet"]:
    if any(fragment in normalized for fragment in ("sheet", "foglio", "tabella", "xlsx", "csv")):
        return "sheet"
    return "doc"


def _truncate_document_segment(raw_value: str, *, stop_on_title_tokens: bool = False) -> str | None:
    cleaned = re.sub(r"\s+", " ", raw_value).strip()
    if not cleaned:
        return None
    if stop_on_title_tokens:
        pattern = r"\s+(?:nominat[oa]|chiamat[oa]|titolo|dove|che|con|in\s+cui|usando|partendo)\b"
    else:
        pattern = r"\s+(?:dove|che|con|in\s+cui|usando|partendo)\b"
    parts = re.split(pattern, cleaned, maxsplit=1, flags=re.IGNORECASE)
    value = parts[0].strip(" .,-")
    return value or None


def _extract_document_title(message: str, *, kind: Literal["doc", "sheet"]) -> str | None:
    named = re.search(r"\b(?:nominat[oa]|chiamat[oa]|chiamalo|chiamala|titolo|con\s+nome|nome\s+file)\s+(.+)$", message, re.IGNORECASE)
    if named:
        cleaned = _truncate_document_segment(named.group(1))
        if cleaned:
            return cleaned.strip(" '\"") or None

    explicit = re.search(r"\btitolo\s+([A-Za-zÀ-ÿ0-9'\" .,:;!?()/_-]{3,200})$", message, re.IGNORECASE)
    if explicit:
        cleaned = re.sub(r"\s+", " ", explicit.group(1)).strip(" .,-")
        return cleaned.strip(" '\"") or None

    normalized = _normalize_text(message)
    if "ordini" in normalized and "2025" in normalized:
        return "Analisi ordini 2025"
    if "prenot" in normalized and "stasera" in normalized:
        return "Situazione prenotazioni stasera"
    return None


def _extract_document_destination(message: str) -> str | None:
    match = re.search(r"\b(?:nella\s+|nel\s+)?(?:cartella|folder|drive)\s+(.+)$", message, re.IGNORECASE)
    if not match:
        return None
    return _truncate_document_segment(match.group(1), stop_on_title_tokens=True)


def _extract_document_prompt(message: str) -> str:
    explicit_brief = re.search(r"^\s*(?:dove|che|in\s+cui)\s+(.+)$", message, re.IGNORECASE)
    if explicit_brief:
        cleaned = re.sub(r"\s+", " ", explicit_brief.group(1)).strip(" .,-")
        return cleaned or message.strip()

    cleaned = re.sub(
        r"\b(?:crea|creare|creami|genera|prepara|preparare|preparami|prepari|salva|salvare|fammi|fai|apri|scrivi|scrivere)\b",
        " ",
        message,
        flags=re.IGNORECASE,
    )
    cleaned = re.sub(
        r"\b(?:un|una|uno|il|lo|la|google|doc|docs|documento|documenti|sheet|sheets|foglio|file)\b",
        " ",
        cleaned,
        flags=re.IGNORECASE,
    )
    cleaned = re.sub(r"\b(?:nella\s+|nel\s+)?(?:cartella|folder|drive)\s+.+$", " ", cleaned, flags=re.IGNORECASE)
    cleaned = re.sub(
        r"\b(?:nominat[oa]|chiamat[oa]|chiamalo|chiamala|titolo|con\s+nome|nome\s+file)\s+[A-Za-zÀ-ÿ0-9'\" .,:;!?()/_-]{1,200}?(?=\s+(?:dove|che|con|in\s+cui|usando|partendo)\b|$)",
        " ",
        cleaned,
        flags=re.IGNORECASE,
    )
    cleaned = re.sub(r"^\s*(?:voglio|vorrei)\s+(?:che\s+mi\s+)?", "", cleaned, flags=re.IGNORECASE)
    cleaned = re.sub(r"\s+", " ", cleaned).strip(" .,-")
    return cleaned or message.strip()


def _is_document_create_request(normalized: str) -> bool:
    has_file_target = any(fragment in normalized for fragment in ("document", "google doc", "doc", "foglio", "sheet", "file"))
    has_creation_intent = any(fragment in normalized for fragment in ("crea", "genera", "prepara", "preparami", "prepari", "salva"))
    return has_file_target and has_creation_intent


def _document_request_needs_grounded_data(message: str, normalized: str) -> bool:
    if not _is_document_create_request(normalized):
        return False
    if _is_tenant_user_list_request(normalized):
        return True
    reduced = normalized
    for token in ("document", "documento", "documenti", "google doc", "doc", "foglio", "sheet", "file", "crea", "genera", "prepara", "preparami", "prepari", "salva"):
        reduced = reduced.replace(token, " ")
    reduced = " ".join(reduced.split())
    return _is_grounded_data_request(message, reduced) if reduced else False


def _build_document_create_tool_call(message: str) -> PlannedToolCall:
    normalized = _normalize_text(message)
    kind = _extract_document_kind(normalized)
    arguments: dict[str, object] = {
        "kind": kind,
        "prompt": _extract_document_prompt(message),
    }
    title = _extract_document_title(message, kind=kind)
    destination_folder_id = _extract_document_destination(message)
    if title:
        arguments["title"] = title
    if destination_folder_id:
        arguments["destination_folder_id"] = destination_folder_id
    return PlannedToolCall(tool="create_google_workspace_document", arguments=arguments)


def _is_orders_document_request(message: str, normalized: str) -> bool:
    if not _is_document_create_request(normalized):
        return False
    if _is_catalog_product_document_request(message):
        return False
    if any(fragment in normalized for fragment in (" ordine ", " ordini ", "acquisti", "storico ordini", "storico acquisti")):
        return True
    return _contains_normalized_word(normalized, "ordine", "ordini", "acquisto", "acquisti")


def _is_catalog_product_document_request(message: str) -> bool:
    prompt = _extract_document_prompt(message)
    normalized_prompt = _normalize_text(prompt)
    if not normalized_prompt:
        return False

    has_product_shape = (
        any(fragment in normalized_prompt for fragment in ("lista", "elenco", "elenc", "catalogo"))
        or _contains_normalized_word(normalized_prompt, "prodotto", "prodotti", "articolo", "articoli")
    )
    if not has_product_shape:
        return False

    has_supplier = bool(_extract_purchase_supplier_query(prompt))
    asks_prices = any(fragment in normalized_prompt for fragment in ("prezzo", "prezzi", "costo", "costi"))
    explicit_period = (
        _extract_reference_year(prompt) is not None
        or _extract_reference_month(prompt) is not None
        or _extract_reference_week_range(prompt) is not None
    )
    historical_shape = any(
        fragment in normalized_prompt
        for fragment in (
            "storico ordini",
            "storico acquisti",
            "ordini di",
            "ordini del",
            "ordini da",
            "righe ordine",
            "batch ordini",
        )
    )

    if explicit_period or historical_shape:
        return False
    return has_supplier or asks_prices


def _extract_grounded_document_product_query(message: str) -> str:
    ignored_tokens = _PRODUCT_QUERY_IGNORED_TOKENS | {
        "acquisto",
        "acquisti",
        "articolo",
        "articoli",
        "catalogo",
        "compro",
        "comprensiva",
        "comprensive",
        "comprensivi",
        "comprensivo",
        "economica",
        "economiche",
        "economici",
        "economico",
        "elenco",
        "lista",
        "meno",
        "ordinata",
        "ordinati",
        "ordinato",
        "ordinate",
        "ordinati",
        "piu",
        "prezzo",
        "prezzi",
        "relativa",
        "relative",
        "relativi",
        "relativo",
        "crescente",
        "costosa",
        "costose",
        "costosi",
        "costoso",
    }

    def clean_candidate(candidate: str) -> str:
        tokens = [
            token
            for token in _tokenize_query(candidate)
            if token
            and token not in ignored_tokens
            and not re.fullmatch(r"20\d{2}", token)
        ]
        return " ".join(tokens[:6]).strip()

    prompt = _extract_document_prompt(message)
    supplier_query = _extract_purchase_supplier_query(prompt)
    if supplier_query:
        return supplier_query
    explicit_match = re.search(
        r"\b(.+?)\s+che\s+(?:ho\s+tra\s+i\s+prodotti|compriamo|acquistiamo|teniamo|usiamo)\b",
        prompt,
        re.IGNORECASE,
    )
    if explicit_match:
        candidate = clean_candidate(explicit_match.group(1))
        if candidate:
            return candidate
    return clean_candidate(prompt) or _extract_product_query(prompt)


def _extract_grounded_document_order_query(message: str) -> str:
    prompt = _extract_document_prompt(message)
    query = _extract_purchase_query(prompt)
    generic_tokens = {
        "ordine",
        "ordini",
        "acquisto",
        "acquisti",
        "storico",
        "tutta",
        "tutte",
        "tutti",
        "tutto",
    }
    query_tokens = query.split()
    if not query_tokens:
        return ""
    if set(query_tokens).issubset(generic_tokens):
        return ""
    return query


def _build_grounded_document_tool_calls(message: str) -> list[PlannedToolCall]:
    normalized = _normalize_text(message)
    if _is_tenant_user_list_request(normalized):
        return [PlannedToolCall(tool="list_tenant_users", arguments={}), _build_document_create_tool_call(message)]

    if _is_tips_request(message, normalized):
        return [_build_tips_tool_call(message), _build_document_create_tool_call(message)]

    if _is_fiscal_documents_request(normalized):
        return [PlannedToolCall(tool="list_fiscal_documents", arguments={}), _build_document_create_tool_call(message)]

    if _is_timeclock_request(normalized):
        return [_build_timeclock_summary_tool_call(message, normalized), _build_document_create_tool_call(message)]

    if _is_homemade_stock_consumption_request(normalized):
        return [_build_homemade_stock_consumption_tool_call(message), _build_document_create_tool_call(message)]

    if _is_inventory_consumption_estimation_request(normalized) and _has_explicit_inventory_estimation_scope(message, normalized):
        return [_build_inventory_consumption_estimation_tool_call(message), _build_document_create_tool_call(message)]

    if _is_inventory_consumption_request(message, normalized):
        return [_build_inventory_consumption_tool_call(message), _build_document_create_tool_call(message)]

    if _is_inventory_request(message, normalized):
        return [_build_inventory_tool_call(message), _build_document_create_tool_call(message)]

    if _is_homemade_request(message, normalized):
        return [_build_homemade_tool_call(message), _build_document_create_tool_call(message)]

    if _is_prenotazioni_request(message, normalized) and not _is_reservation_write_request(normalized):
        target_date = _extract_explicit_date(message)
        return [
            PlannedToolCall(
                tool="list_reservations",
                arguments={
                    "date": target_date.isoformat() if target_date is not None else None,
                    "limit": _CHAT_LIST_LIMIT,
                },
            ),
            _build_document_create_tool_call(message),
        ]

    if _is_orders_document_request(message, normalized):
        query = _extract_grounded_document_order_query(message)
        year = _extract_reference_year(message)
        month = _extract_reference_month(message)
        tool_name = "get_purchase_history" if any(keyword in normalized for keyword in ("righe", "dettaglio", "dettagli", "linee")) else "get_purchase_batches"
        return [
            PlannedToolCall(
                tool=tool_name,
                arguments={
                    "query": query,
                    "year": year,
                    "month": month,
                    "limit": 5000,
                },
            ),
            _build_document_create_tool_call(message),
        ]

    query = _extract_grounded_document_product_query(message)
    if not query:
        return [_build_document_create_tool_call(message)]
    return [
        PlannedToolCall(
            tool="search_products",
            arguments={
                "query": query,
                "limit": 5000,
            },
        ),
        _build_document_create_tool_call(message),
    ]


def _build_capability_reply(message: str, normalized: str) -> str | None:
    if _is_sales_data_request_unavailable(normalized):
        return (
            "Posso gestire analisi e confronti del venduto solo quando colleghiamo una fonte reale dalla cassa. "
            "Oggi dalla Home leggo in modo affidabile catalogo prodotti, cataloghi fornitori, ordini storici, prenotazioni, note, obiettivi e documenti, "
            "ma non ho ancora dati reali di venduto o incasso."
        )
    if _is_inventory_consumption_estimation_request(normalized) and _is_capability_question(normalized):
        return (
            "Sì, posso stimare i consumi giornalieri per prodotto usando ordini storici e inventari, ma devo indicarti il livello di affidabilità. "
            "La formula corretta è: giacenza iniziale + acquisti del periodo - giacenza finale, diviso i giorni del periodo. "
            "Se manca una giacenza iniziale reale, posso fare solo una stima parziale usando gli acquisti 2025 e il primo inventario 2026 come rimanenza finale: è utile per orientarsi, ma non è un consumo preciso perché non sappiamo quanto stock era già presente a inizio 2025. "
            "Per una stima commerciale solida conviene salvare inventari periodici: da lì posso calcolare consumi medi giornalieri, prodotti più lenti, prodotti più veloci e fabbisogno di riordino."
        )
    if _is_product_capability_question(message, normalized):
        return (
            "Sì. Posso anche aggiungere o aggiornare prodotti dal gestionale centrale, "
            "Per registrarlo bene scrivimi almeno il nome prodotto. "
            "Se li hai, aggiungi anche lotto, fornitore, codice, prezzo, iva, categoria e note. "
            "Se il lotto e un cartone o una cassa, dimmi anche quante unita contiene. "
            "Se qualche dato ti manca, lo salvo comunque e lo completiamo dopo."
        )
    return None


def _latest_user_message(conversation: list[dict[str, str]]) -> str:
    for message in reversed(conversation):
        if message.get("role") == "user":
            return str(message.get("content") or "")
    return ""


def _latest_timeclock_request_message(conversation: list[dict[str, str]]) -> str:
    for message in reversed(conversation[-10:]):
        if message.get("role") != "user":
            continue
        content = str(message.get("content") or "")
        normalized = _normalize_text(content)
        if _is_timeclock_request(normalized):
            return content
        catalog_query = _extract_catalog_query(content)
        has_other_domain = (
            any(keyword in normalized for keyword in _ORDERS_KEYWORDS)
            or any(keyword in normalized for keyword in _INVENTORY_KEYWORDS)
            or _is_tips_request(content, normalized)
            or any(keyword in normalized for keyword in _RESERVATION_SUBJECT_KEYWORDS)
            or _is_homemade_request(content, normalized)
            or bool(catalog_query and _is_catalog_request(content, normalized, catalog_query))
        )
        if has_other_domain:
            return ""
        if _is_short_followup_query(content):
            continue
    return ""


def _latest_assistant_message(conversation: list[dict[str, str]]) -> str:
    for message in reversed(conversation):
        if message.get("role") == "assistant":
            return str(message.get("content") or "")
    return ""


def _is_short_followup_query(message: str) -> bool:
    normalized = _normalize_text(message)
    if not normalized:
        return False
    tokens = normalized.split()
    if tokens and tokens[0] == "e":
        tokens = tokens[1:]
    return 0 < len(tokens) <= 4


def _is_purchase_followup_request(normalized_current: str) -> bool:
    return (
        any(keyword in normalized_current for keyword in ("ordine", "ordini", "acquist", "compr"))
        and any(keyword in normalized_current for keyword in ("singol", "songol", "dettagl", "mostr", "elenc", "righe", "storico"))
    )


def _is_purchase_expand_followup_request(normalized_current: str) -> bool:
    if not any(keyword in normalized_current for keyword in ("mostr", "elenc", "lista", "tutt", "tutte", "tutto", "inter", "complet")):
        return False
    return any(token in normalized_current for token in ("tutti", "tutte", "tutto", "completa", "completo", "intera", "intero"))


def _assistant_mentions_missing_price_variants(conversation: list[dict[str, str]]) -> bool:
    for message in reversed(conversation[-8:]):
        if message.get("role") != "assistant":
            continue
        normalized = _normalize_text(str(message.get("content") or ""))
        if "prezzi mancanti non inclusi nella stima" in normalized or "non ho abbastanza prezzi salvati" in normalized:
            return True
    return False


def _is_missing_price_variants_followup_request(normalized_current: str, conversation: list[dict[str, str]]) -> bool:
    if not any(keyword in normalized_current for keyword in ("mostr", "elenc", "lista", "quali", "quali sono")):
        return False
    if "variant" not in normalized_current and "quell" not in normalized_current:
        return False
    return _assistant_mentions_missing_price_variants(conversation)


def _is_missing_price_variants_reference_message(normalized_current: str) -> bool:
    if not any(keyword in normalized_current for keyword in ("mostr", "elenc", "lista", "quali", "quali sono")):
        return False
    return any(keyword in normalized_current for keyword in ("variant", "quell", "mancant", "senza prezzo"))


def _is_generic_purchase_followup_query(query: str) -> bool:
    if not query.strip():
        return True
    generic_tokens = {
        "all",
        "allora",
        "attuale",
        "attuali",
        "confronta",
        "confrontami",
        "confrontare",
        "confronto",
        "confronti",
        "contro",
        "corrente",
        "mese",
        "mesi",
        "quest",
        "questa",
        "queste",
        "questi",
        "questo",
        "riispetto",
        "rispetto",
        "separata",
        "separate",
        "separati",
        "separato",
        "singolo",
        "singoli",
        "songolo",
        "songoli",
        "ordine",
        "ordini",
        "dettaglio",
        "dettagli",
        "storico",
        "riga",
        "righe",
        "variante",
        "varianti",
        "quella",
        "quelle",
        "quello",
        "quelli",
        "questa",
        "queste",
        "questo",
        "questi",
        "mancante",
        "mancanti",
        "senza",
        "prezzo",
        "prezzi",
    }
    numeric_tokens = {
        "un",
        "uno",
        "una",
        "due",
        "tre",
        "quattro",
        "cinque",
        "sei",
        "sette",
        "otto",
        "nove",
        "dieci",
    }
    query_tokens = {
        token
        for token in query.split()
        if token and not token.isdigit() and token not in numeric_tokens
    }
    return bool(query_tokens) and query_tokens.issubset(generic_tokens)


def _is_generic_catalog_followup_query(query: str) -> bool:
    if not query.strip():
        return True
    generic_tokens = {
        "articolo",
        "articoli",
        "catalogo",
        "cataloghi",
        "costa",
        "costano",
        "prezzo",
        "prezzi",
        "prodotto",
        "prodotti",
        "quanto",
        "quanta",
        "quanti",
        "quante",
        "questo",
        "questa",
        "quello",
        "quella",
    }
    query_tokens = {token for token in _catalog_query_tokens(query) if token}
    return bool(query_tokens) and query_tokens.issubset(generic_tokens)


def _merge_product_write_fragments(
    message: str,
    conversation: list[dict[str, str]],
    thread_state: dict[str, object] | None = None,
) -> dict[str, object]:
    state = _normalize_home_thread_state(thread_state)
    merged: dict[str, object] = {}
    if isinstance(state.get("pending_product"), dict):
        merged.update(state["pending_product"])
    for item in conversation[-6:]:
        if item.get("role") != "user":
            continue
        merged.update(_parse_product_write_fragment(str(item.get("content") or "")))
    merged.update(_parse_product_write_fragment(message))
    fallback_product_name = str(merged.get("product_name") or "").strip()
    if not fallback_product_name:
        state_catalog_query = str(state.get("catalog_query") or "").strip()
        if state_catalog_query:
            merged["product_name"] = state_catalog_query
        else:
            previous_catalog_query = _latest_catalog_subject_query(conversation)
            if previous_catalog_query:
                merged["product_name"] = previous_catalog_query
    if not merged.get("product_name"):
        fallback_name = _extract_product_name_for_write(message)
        if fallback_name:
            fallback_name = re.sub(
                r"\b\d+(?:[.,]\d+)?\s+unita?\s+per\s+(?:cartone|cassa)\b",
                " ",
                fallback_name,
                flags=re.IGNORECASE,
            )
            fallback_name = re.sub(r"\s+", " ", fallback_name).strip(" .,-")
            merged["product_name"] = fallback_name
    return merged


def _conversation_suggests_pending_product_write(
    message: str,
    conversation: list[dict[str, str]],
    thread_state: dict[str, object] | None = None,
) -> bool:
    normalized = _normalize_text(message)
    if _is_sales_goal_read_request(normalized) or _is_sales_goal_write_request(normalized):
        return False
    if _is_product_write_request(message, normalized):
        return True

    state = _normalize_home_thread_state(thread_state)
    if str(state.get("pending_action") or "").strip() == "sales_goal_write":
        return False
    if str(state.get("pending_action") or "").strip() == "product_write":
        return True

    last_user = _latest_user_message(conversation)
    if last_user and _is_product_write_request(last_user, _normalize_text(last_user)):
        return True

    for item in reversed(conversation[-6:]):
        if item.get("role") != "assistant":
            continue
        content = _normalize_text(str(item.get("content") or ""))
        if any(hint in content for hint in _PRODUCT_WRITE_ASSISTANT_HINTS):
            return True
    return False


def _build_product_write_clarification(
    message: str,
    conversation: list[dict[str, str]],
    thread_state: dict[str, object] | None = None,
) -> str | None:
    if not _conversation_suggests_pending_product_write(message, conversation, thread_state):
        return None

    merged = _merge_product_write_fragments(message, conversation, thread_state)
    operation = str(merged.get("operation") or "upsert").strip()
    normalized = _normalize_text(message)
    state = _normalize_home_thread_state(thread_state)
    last_product_write = state.get("last_product_write") if isinstance(state.get("last_product_write"), dict) else {}

    if operation == "delete":
        if _is_relative_new_product_delete_request(normalized) and str(last_product_write.get("status") or "").strip() != "created":
            return (
                "Non risulta nessun nuovo prodotto appena creato da eliminare. "
                "Se vuoi rimuovere un prodotto esistente, dimmi il nome del prodotto e, se serve, anche lotto o fornitore."
            )
        if merged.get("product_name"):
            return None
        return "Dimmi quale prodotto vuoi eliminare."

    if merged.get("product_name"):
        lot_code = _clean_optional_text(merged.get("lot_code") if isinstance(merged.get("lot_code"), str) else None)
        units_per_pack = merged.get("units_per_pack")
        if _lot_requires_units_per_pack(lot_code) and _coerce_positive_float(units_per_pack) is None:
            if _message_allows_missing_units_per_pack(message):
                return None
            return (
                "Per il lotto "
                f"{lot_code.upper() if lot_code else 'CT'} mi serve sapere quante unita contiene un cartone. "
                "Questo dato serve per conteggi, litri e obiettivi. "
                "Se non lo sai adesso, scrivimi 'salva comunque' e lo completiamo dopo."
            )
        return None

    return (
        "Certo. Per registrare il prodotto scrivimi almeno il nome prodotto. "
        "Se li hai, aggiungi anche lotto, fornitore, codice, prezzo, iva, categoria e note. "
        "Se il lotto e un cartone o una cassa, dimmi anche quante unita contiene. "
        "Se qualche dato ti manca, lo salvo comunque e lo completiamo dopo."
    )


def _build_pack_size_followup_clarification(
    message: str,
    conversation: list[dict[str, str]],
    thread_state: dict[str, object] | None = None,
) -> str | None:
    context = _latest_pack_size_context(conversation) or _latest_pack_size_context_from_state(thread_state)
    if context is None:
        return None

    normalized = _normalize_text(message)
    if not _is_pack_size_update_intent(normalized):
        return None

    product_name = context.get("product_name") or "questo prodotto"
    supplier_name = context.get("supplier_name") or "fornitore non indicato"
    lot_code = context.get("lot_code") or ""
    if _lot_requires_units_per_pack(lot_code):
        return (
            f"Per {product_name} dimmi quante unita contiene il cartone. "
            "Per esempio puoi scrivermi: "
            f"'{product_name} lotto {lot_code or 'ct'} fornitore {supplier_name}, 6 unita per cartone'."
        )

    return (
        f"Per {product_name} il lotto attuale e {lot_code or 'bt'}, quindi non serve un valore unita per pack. "
        "Se vuoi registrare anche il cartone, scrivimi per esempio: "
        f"'{product_name} lotto ct fornitore {supplier_name}, 6 unita per cartone'."
    )


def _is_relative_new_product_delete_request(normalized: str) -> bool:
    return any(
        fragment in normalized
        for fragment in (
            "questo nuovo prodotto",
            "questa nuova scheda prodotto",
            "quel nuovo prodotto",
            "cancella il nuovo prodotto",
            "elimina il nuovo prodotto",
        )
    )


def _build_contextual_product_write_tool_calls(
    message: str,
    conversation: list[dict[str, str]],
    thread_state: dict[str, object] | None = None,
) -> list[PlannedToolCall]:
    if not _conversation_suggests_pending_product_write(message, conversation, thread_state):
        return []

    normalized = _normalize_text(message)
    merged = _merge_product_write_fragments(message, conversation, thread_state)
    state = _normalize_home_thread_state(thread_state)
    last_product_write = state.get("last_product_write") if isinstance(state.get("last_product_write"), dict) else {}

    if (
        str(merged.get("operation") or "upsert").strip() == "delete"
        and _is_relative_new_product_delete_request(normalized)
    ):
        if str(last_product_write.get("status") or "").strip() == "created":
            for key in ("product_name", "lot_code", "supplier_name"):
                value = str(last_product_write.get(key) or "").strip()
                if value:
                    merged[key] = value
        else:
            return []

    if not merged or not merged.get("product_name"):
        return []
    return [PlannedToolCall(tool="upsert_product", arguments=merged)]


def _build_contextual_pack_size_tool_calls(
    message: str,
    conversation: list[dict[str, str]],
    thread_state: dict[str, object] | None = None,
) -> list[PlannedToolCall]:
    context = _latest_pack_size_context(conversation) or _latest_pack_size_context_from_state(thread_state)
    if context is None:
        return []

    parsed = _parse_product_write_fragment(message)
    units_per_pack = _coerce_positive_float(parsed.get("units_per_pack"))
    if units_per_pack is None:
        return []

    lot_code = _clean_optional_text(parsed.get("lot_code") if isinstance(parsed.get("lot_code"), str) else None) or context.get("lot_code")
    if not _lot_requires_units_per_pack(lot_code):
        return []

    supplier_name = _clean_optional_text(parsed.get("supplier_name") if isinstance(parsed.get("supplier_name"), str) else None) or context.get("supplier_name")
    product_name = _clean_optional_text(parsed.get("product_name") if isinstance(parsed.get("product_name"), str) else None) or context.get("product_name")
    if not product_name:
        return []

    arguments: dict[str, object] = {
        "product_name": product_name,
        "lot_code": lot_code,
        "supplier_name": supplier_name,
        "units_per_pack": units_per_pack,
    }
    return [PlannedToolCall(tool="upsert_product", arguments=arguments)]


def _conversation_suggests_pending_sales_goal_write(
    message: str,
    conversation: list[dict[str, str]],
    thread_state: dict[str, object] | None = None,
) -> bool:
    normalized = _normalize_text(message)
    if _is_sales_goal_read_request(normalized):
        return False
    if _is_sales_goal_write_request(normalized):
        return True

    state = _normalize_home_thread_state(thread_state)
    if str(state.get("pending_action") or "").strip() == "sales_goal_write":
        return True

    last_user = _latest_user_message(conversation)
    if last_user and _is_sales_goal_write_request(_normalize_text(last_user)):
        return True

    for item in reversed(conversation[-6:]):
        if item.get("role") != "assistant":
            continue
        content = _normalize_text(str(item.get("content") or ""))
        if any(hint in content for hint in _SALES_GOAL_ASSISTANT_HINTS):
            return True
    return False


def _merge_sales_goal_write_fragments(
    message: str,
    conversation: list[dict[str, str]],
    thread_state: dict[str, object] | None = None,
) -> dict[str, object]:
    state = _normalize_home_thread_state(thread_state)
    normalized_message = _normalize_text(message)
    starts_new_goal_request = _is_sales_goal_write_request(normalized_message)
    merged: dict[str, object] = {}
    if not starts_new_goal_request and isinstance(state.get("pending_sales_goal"), dict):
        merged.update(state["pending_sales_goal"])
    relevant_conversation = [] if starts_new_goal_request else conversation[-6:]
    for item in relevant_conversation:
        if item.get("role") != "user":
            continue
        merged.update(_parse_sales_goal_write_fragment(str(item.get("content") or "")))
    if not (
        _extract_product_units_per_pack_for_write(message) is not None
        and not _contains_goal_keyword(normalized_message)
    ):
        merged.update(_parse_sales_goal_write_fragment(message))
    return merged


def _build_contextual_sales_goal_write_tool_calls(
    message: str,
    conversation: list[dict[str, str]],
    thread_state: dict[str, object] | None = None,
) -> list[PlannedToolCall]:
    if not _conversation_suggests_pending_sales_goal_write(message, conversation, thread_state):
        return []

    merged = _merge_sales_goal_write_fragments(message, conversation, thread_state)

    meaningful_fields = {
        "goal_id",
        "name",
        "goal_type",
        "description",
        "product_match",
        "secondary_product_match",
        "supplier_match",
        "target",
        "secondary_target",
    }
    if not any(key in merged for key in meaningful_fields):
        return []
    return [PlannedToolCall(tool="write_sales_goal", arguments=merged)]


def _latest_tips_request_message(conversation: list[dict[str, str]]) -> str:
    for item in reversed(conversation[-8:]):
        if item.get("role") != "user":
            continue
        content = str(item.get("content") or "")
        if _is_tips_request(content, _normalize_text(content)):
            return content
    return ""


def _assistant_mentions_tips_context(conversation: list[dict[str, str]]) -> bool:
    for item in reversed(conversation[-6:]):
        if item.get("role") != "assistant":
            continue
        normalized = _normalize_text(str(item.get("content") or ""))
        if any(
            hint in normalized
            for hint in (
                "giornate mance salvate",
                "storico mance trovato",
                "giornata mance",
                "totale visibile nei calcoli",
                "di mance giornata su",
            )
        ):
            return True
    return False


def _is_tips_followup_request(message: str, normalized: str) -> bool:
    if _is_tips_request(message, normalized):
        return False
    return any(
        fragment in normalized
        for fragment in (
            "somma",
            "totale",
            "in tutto",
            "fai la somma",
            "fammi una somma",
            "quanto fanno",
            "quanto fa",
        )
    )


def _build_contextual_tips_tool_calls(
    message: str,
    conversation: list[dict[str, str]],
) -> list[PlannedToolCall]:
    normalized = _normalize_text(message)
    if not _is_tips_followup_request(message, normalized):
        return []
    if not _assistant_mentions_tips_context(conversation):
        return []

    previous_tips_message = _latest_tips_request_message(conversation)
    if not previous_tips_message:
        return []

    inherited_normalized = _normalize_text(previous_tips_message)
    inherited_area = "sala" if "sala" in inherited_normalized else "bar" if _contains_normalized_word(inherited_normalized, "bar") else None
    inherited_year = _extract_reference_year(previous_tips_message)
    inherited_month = _extract_reference_month(previous_tips_message)
    query_source = previous_tips_message
    return [
        _build_tips_tool_call(
            query_source,
            fallback_area=inherited_area,
            fallback_year=inherited_year,
            fallback_month=inherited_month,
        )
    ]


def _latest_sales_goal_request_message(conversation: list[dict[str, str]]) -> str:
    for item in reversed(conversation[-8:]):
        if item.get("role") != "user":
            continue
        content = str(item.get("content") or "")
        if _is_sales_goal_read_request(_normalize_text(content)):
            return content
    return ""


def _assistant_mentions_sales_goals(conversation: list[dict[str, str]]) -> bool:
    for item in reversed(conversation[-6:]):
        if item.get("role") != "assistant":
            continue
        normalized = _normalize_text(str(item.get("content") or ""))
        if "obiettivi" in normalized and "configurati nel locale" in normalized:
            return True
    return False


def _build_contextual_sales_goal_read_tool_calls(
    message: str,
    conversation: list[dict[str, str]],
    thread_state: dict[str, object] | None = None,
) -> list[PlannedToolCall]:
    normalized = _normalize_text(message)
    if not _is_sales_goal_graph_request(normalized):
        return []
    if not _assistant_mentions_sales_goals(conversation):
        return []

    previous_goal_message = _latest_sales_goal_request_message(conversation)
    state = _normalize_home_thread_state(thread_state)
    inherited_year = _extract_explicit_year(previous_goal_message) if previous_goal_message else None
    if inherited_year is None:
        inherited_year = _coerce_optional_year(state.get("sales_goals_year"))
    return [PlannedToolCall(tool="get_sales_goals", arguments={"year": inherited_year})]


def _extract_homemade_context_from_assistant_message(content: str) -> str | None:
    match = re.search(r"Ricetta homemade:\s*(.+)", content, re.IGNORECASE)
    if not match:
        return None
    value = match.group(1).strip()
    return value or None


def _latest_homemade_context(conversation: list[dict[str, str]]) -> str | None:
    for item in reversed(conversation):
        if item.get("role") != "assistant":
            continue
        context = _extract_homemade_context_from_assistant_message(str(item.get("content") or ""))
        if context:
            return context
    return None


def _build_contextual_homemade_tool_calls(message: str, conversation: list[dict[str, str]]) -> list[PlannedToolCall]:
    last_recipe_name = _latest_homemade_context(conversation)
    if not last_recipe_name:
        return []
    normalized = _normalize_text(message)
    if not (
        _extract_liters_from_text(message) is not None
        or re.match(r"^\s*e\s+per\b", message, re.IGNORECASE)
        or any(fragment in normalized for fragment in ("ricetta", "prep", "homemade", "preparazione"))
    ):
        return []
    extracted_query = _extract_homemade_query(message).strip()
    inherited_query = extracted_query if _is_meaningful_homemade_query(extracted_query) else last_recipe_name
    if not inherited_query:
        return []
    return [_build_homemade_tool_call(message, query_override=inherited_query)]


def _build_sales_goal_confirmation_text(arguments: dict[str, object]) -> str | None:
    goal_type = str(arguments.get("goal_type") or "").strip()
    name = _clean_optional_text(arguments.get("name") if isinstance(arguments.get("name"), str) else None)
    supplier_match = _clean_optional_text(arguments.get("supplier_match") if isinstance(arguments.get("supplier_match"), str) else None)
    product_match = _clean_optional_text(arguments.get("product_match") if isinstance(arguments.get("product_match"), str) else None)
    description = _clean_optional_text(arguments.get("description") if isinstance(arguments.get("description"), str) else None)
    year = arguments.get("year")
    target = arguments.get("target")
    unit_label = _clean_optional_text(arguments.get("unit_label") if isinstance(arguments.get("unit_label"), str) else None)

    if goal_type not in {"quantity", "liters", "liters_dual", "note"}:
        return None
    if goal_type == "note" and not description:
        return None
    if goal_type != "note" and not isinstance(target, (int, float)):
        return None
    if goal_type in {"quantity", "liters", "liters_dual"} and not (product_match or supplier_match):
        return None

    year_label = str(int(year)) if isinstance(year, int) else str(_today_in_timezone().year)
    scope_bits: list[str] = []
    product_terms = _split_goal_match_terms(product_match)
    if product_match:
        if len(product_terms) > 1:
            scope_bits.append(f"sui prodotti {', '.join(product_terms)}")
        else:
            scope_bits.append(f"sul prodotto {product_match}")
    if supplier_match:
        scope_bits.append(f"del fornitore {supplier_match}" if product_match else f"sul fornitore {supplier_match}")
    scope_label = " ".join(scope_bits).strip()

    target_label = None
    if isinstance(target, (int, float)):
        target_label = str(int(target)) if float(target).is_integer() else str(round(float(target), 2)).replace(".", ",")
    unit = unit_label or ("L" if goal_type == "liters" else "articoli")

    if goal_type == "note":
        label_bits = [f"la nota obiettivo {year_label}"]
        if name:
            label_bits.append(f"'{name}'")
        label_bits.append(f"con testo: {description}")
        return "Per essere preciso: vuoi impostare " + " ".join(label_bits) + ". Se e corretto, scrivi CONFERMO. Altrimenti dimmi cosa cambiare."

    label_bits = [f"l'obiettivo {year_label}"]
    if name:
        label_bits.append(f"'{name}'")
    if scope_label:
        label_bits.append(scope_label)
    if target_label is not None and goal_type != "note":
        label_bits.append(f"con target {target_label} {unit}".strip())
    return "Per essere preciso: vuoi impostare " + " ".join(label_bits) + ". Se e corretto, scrivi CONFERMO. Altrimenti dimmi cosa cambiare."


def _build_sales_goal_write_clarification(
    session: SessionIdentity,
    message: str,
    conversation: list[dict[str, str]],
    thread_state: dict[str, object] | None = None,
) -> str | None:
    normalized = _normalize_text(message)
    if _CONFIRMATION_PATTERN.match(message.strip()):
        return None
    if _is_sales_goal_read_request(normalized):
        return None
    if not (_is_sales_goal_write_request(normalized) or _conversation_suggests_pending_sales_goal_write(message, conversation, thread_state)):
        return None

    merged = _merge_sales_goal_write_fragments(message, conversation, thread_state)
    goal_type = str(merged.get("goal_type") or "").strip()
    has_target = isinstance(merged.get("target"), (int, float))
    has_scope = bool(_clean_optional_text(merged.get("product_match") if isinstance(merged.get("product_match"), str) else None)) or bool(
        _clean_optional_text(merged.get("supplier_match") if isinstance(merged.get("supplier_match"), str) else None)
    )

    if merged.get("operation") == "delete":
        return None
    if goal_type not in {"quantity", "liters", "liters_dual", "note"}:
        return "Per impostare bene l'obiettivo dimmi se va misurato in quantita, litri oppure se e solo una nota."
    if goal_type == "note" and not _clean_optional_text(merged.get("description") if isinstance(merged.get("description"), str) else None):
        return "Per salvare una nota obiettivo dimmi anche il testo completo che vuoi registrare."
    if goal_type != "note" and not has_target:
        return "Per impostare bene l'obiettivo mi serve anche il target numerico."
    if goal_type in {"quantity", "liters", "liters_dual"} and not has_scope:
        return "Per impostare bene l'obiettivo indicami almeno il prodotto oppure il fornitore da monitorare."

    if goal_type in {"liters", "liters_dual"}:
        with _connect_orders_database(session) as connection:
            missing_pack_sizes = _find_liters_goal_missing_pack_sizes(
                connection,
                supplier_match=_clean_optional_text(merged.get("supplier_match") if isinstance(merged.get("supplier_match"), str) else None),
                product_match=_clean_optional_text(merged.get("product_match") if isinstance(merged.get("product_match"), str) else None),
                secondary_product_match=_clean_optional_text(merged.get("secondary_product_match") if isinstance(merged.get("secondary_product_match"), str) else None),
            )
        if missing_pack_sizes:
            preview = ", ".join(
                f"{item['product_name']} ({item['lot_code']}, {item['supplier_name']})" for item in missing_pack_sizes
            )
            return (
                "Per contare bene i litri mi serve sapere quante unita contiene il lotto "
                "cartone/cassa di questi prodotti: "
                f"{preview}. "
                "Aggiorna questo dato dal catalogo prodotti oppure dimmelo e poi salvo l'obiettivo."
            )

    confirmation_text = _build_sales_goal_confirmation_text(merged)
    if confirmation_text and _conversation_suggests_pending_sales_goal_write(message, conversation, thread_state):
        return confirmation_text
    return None


def _build_contextual_purchase_comparison_tool_calls(
    message: str,
    conversation: list[dict[str, str]],
    thread_state: dict[str, object] | None = None,
) -> list[PlannedToolCall]:
    last_user_message = _latest_user_message(conversation)
    state = _normalize_home_thread_state(thread_state)
    comparison_state = state.get("comparison") if isinstance(state.get("comparison"), dict) else {}
    if not last_user_message and not comparison_state:
        return []

    normalized_current = _normalize_text(message)
    normalized_previous = _normalize_text(last_user_message)
    periods = _extract_reference_periods(message)
    years = _extract_reference_years(message)
    if len(periods) < 2 and len(years) < 2:
        return []
    if _is_purchase_comparison_request(message, normalized_current):
        return [
            tool_call
            for tool_call in (
                _build_purchase_comparison_tool_call(
                    message,
                    focus_hint=_purchase_comparison_focus(normalized_current, _extract_purchase_query(message)),
                    percentage_requested=("%" in normalized_current or "percentual" in normalized_current),
                ),
            )
            if tool_call is not None
        ]
    if not re.search(r"\b(?:tra|oppure|o|vs|versus)\b", normalized_current):
        return []

    previous_is_comparison = _is_purchase_comparison_request(last_user_message, normalized_previous) or any(
        "confronto storico" in _normalize_text(str(item.get("content") or ""))
        for item in conversation[-4:]
        if item.get("role") == "assistant"
    )
    if comparison_state:
        previous_is_comparison = True
    if not previous_is_comparison:
        return []

    previous_periods = _extract_reference_periods(last_user_message)
    fallback_year = previous_periods[0][0] if previous_periods and previous_periods[0][0] is not None else _extract_reference_year(last_user_message)
    if fallback_year is None:
        fallback_year = _coerce_optional_year(comparison_state.get("primary_year")) or _coerce_optional_year(comparison_state.get("secondary_year"))
    previous_query = _extract_purchase_query(last_user_message) or str(comparison_state.get("query") or "")
    focus_hint = str(comparison_state.get("focus_hint") or "").strip() or _purchase_comparison_focus(normalized_previous, previous_query)
    percentage_requested = bool(comparison_state.get("percentage_requested")) or "%" in normalized_previous or "percentual" in normalized_previous

    comparison_tool_call = _build_purchase_comparison_tool_call(
        message,
        fallback_year=fallback_year,
        fallback_query=previous_query,
        focus_hint=focus_hint,
        percentage_requested=percentage_requested,
    )
    if comparison_tool_call is None:
        return []
    return [comparison_tool_call]


def _has_explicit_non_fiscal_spend_domain(message: str, normalized: str) -> bool:
    if _is_fiscal_spend_request(message, normalized):
        return False
    product_query = _extract_product_query(message)
    catalog_query = _extract_catalog_query(message)
    return any(
        (
            _is_timeclock_request(normalized),
            _is_reservation_subject_request(normalized) or _is_reservation_write_request(normalized),
            _is_tips_request(message, normalized),
            _is_inventory_request(message, normalized),
            _is_homemade_request(message, normalized),
            _is_fiscal_documents_request(normalized),
            _is_tenant_user_list_request(normalized),
            _is_module_settings_read_request(normalized),
            _is_sales_goal_read_request(normalized) or _is_sales_goal_write_request(normalized),
            _is_document_create_request(normalized),
            _is_supplier_catalog_request(normalized),
            bool(catalog_query and _is_catalog_request(message, normalized, catalog_query)),
            _is_catalog_request(message, normalized, product_query),
        )
    )


def _build_contextual_fiscal_spend_tool_calls(
    message: str,
    conversation: list[dict[str, str]],
    thread_state: dict[str, object] | None = None,
) -> list[PlannedToolCall]:
    normalized_current = _normalize_text(message)
    if _is_fiscal_spend_request(message, normalized_current):
        return []
    if _has_explicit_non_fiscal_spend_domain(message, normalized_current):
        return []
    if not (_is_short_followup_query(message) or normalized_current.startswith(("e ", "da ", "dal ", "dalla "))):
        return []

    state = _normalize_home_thread_state(thread_state)
    previous_fiscal_message = _latest_fiscal_spend_request_message(conversation)
    previous_was_fiscal_spend = bool(previous_fiscal_message) or str(state.get("last_tool") or "") == "fiscal_spend_query"
    if not previous_was_fiscal_spend:
        return []

    current_query = _extract_fiscal_spend_query(message)
    if not current_query or _is_generic_purchase_followup_query(current_query):
        return []

    week_range = _extract_reference_week_range(message)
    start_date: str | None = None
    end_date: str | None = None
    if week_range is not None:
        start_date = week_range[0].isoformat()
        end_date = week_range[1].isoformat()

    explicit_period = _message_has_explicit_purchase_period(message)
    year = _extract_reference_year(message)
    month = _extract_reference_month(message)
    if not explicit_period:
        year = year or _coerce_optional_year(state.get("fiscal_spend_year")) or _extract_reference_year(previous_fiscal_message)
        month = month or _coerce_optional_month(state.get("fiscal_spend_month")) or _extract_reference_month(previous_fiscal_message)
        if start_date is None and end_date is None:
            start_date = _coerce_optional_iso_date(state.get("fiscal_spend_start_date"))
            end_date = _coerce_optional_iso_date(state.get("fiscal_spend_end_date"))

    return [
        _build_fiscal_spend_tool_call(
            message,
            query_override=current_query,
            year_override=year,
            month_override=month,
            start_date_override=start_date,
            end_date_override=end_date,
        )
    ]


def _build_contextual_tool_calls(
    message: str,
    conversation: list[dict[str, str]],
    thread_state: dict[str, object] | None = None,
) -> list[PlannedToolCall]:
    normalized_current = _normalize_text(message)
    if _is_sales_goal_read_request(normalized_current):
        return [PlannedToolCall(tool="get_sales_goals", arguments={"year": _extract_explicit_year(message)})]

    reservation_tool_calls = _build_contextual_reservation_create_tool_calls(message, conversation)
    if reservation_tool_calls:
        return reservation_tool_calls

    fiscal_spend_tool_calls = _build_contextual_fiscal_spend_tool_calls(message, conversation, thread_state)
    if fiscal_spend_tool_calls:
        return fiscal_spend_tool_calls

    purchase_comparison_tool_calls = _build_contextual_purchase_comparison_tool_calls(message, conversation, thread_state)
    if purchase_comparison_tool_calls:
        return purchase_comparison_tool_calls

    tips_tool_calls = _build_contextual_tips_tool_calls(message, conversation)
    if tips_tool_calls:
        return tips_tool_calls

    timeclock_tool_calls = _build_contextual_timeclock_tool_calls(message, conversation, thread_state)
    if timeclock_tool_calls:
        return timeclock_tool_calls

    pack_size_tool_calls = _build_contextual_pack_size_tool_calls(message, conversation, thread_state)
    if pack_size_tool_calls:
        return pack_size_tool_calls

    product_tool_calls = _build_contextual_product_write_tool_calls(message, conversation, thread_state)
    if product_tool_calls:
        return product_tool_calls

    sales_goal_read_tool_calls = _build_contextual_sales_goal_read_tool_calls(message, conversation, thread_state)
    if sales_goal_read_tool_calls:
        return sales_goal_read_tool_calls

    sales_goal_tool_calls = _build_contextual_sales_goal_write_tool_calls(message, conversation, thread_state)
    if sales_goal_tool_calls:
        return sales_goal_tool_calls

    state = _normalize_home_thread_state(thread_state)
    if state.get("inventory_consumption_estimation_mode") and _is_homemade_context_switch(normalized_current):
        inherited_query = str(state.get("inventory_consumption_estimation_query") or "").strip()
        return [_build_homemade_stock_consumption_tool_call(message, query_override=inherited_query)]

    previous_homemade_stock_consumption_message = _latest_homemade_stock_consumption_request_message(conversation)
    if previous_homemade_stock_consumption_message and (
        _is_short_followup_query(message)
        or _is_homemade_stock_consumption_request(normalized_current)
        or any(fragment in normalized_current for fragment in ("oggi", "ieri", "giorni", "settimana", "mese", "anno"))
    ):
        inherited_query = _extract_homemade_stock_consumption_query(previous_homemade_stock_consumption_message)
        current_query = _extract_homemade_stock_consumption_query(message)
        return [
            _build_homemade_stock_consumption_tool_call(
                message,
                query_override=current_query or inherited_query,
            )
        ]

    homemade_tool_calls = _build_contextual_homemade_tool_calls(message, conversation)
    if homemade_tool_calls:
        return homemade_tool_calls

    last_user_message = _latest_user_message(conversation)
    if not last_user_message:
        return []

    normalized_previous = _normalize_text(last_user_message)
    previous_query = _extract_purchase_query(last_user_message) or str(state.get("purchase_query") or "")
    current_query = _extract_purchase_query(message).strip()
    current_inventory_query = _extract_inventory_query(message).strip()
    state_purchase_query = str(state.get("purchase_query") or "").strip()
    state_purchase_year = _coerce_optional_year(state.get("purchase_year"))
    state_purchase_month = _coerce_optional_month(state.get("purchase_month"))
    state_purchase_start_date = _coerce_optional_iso_date(state.get("purchase_start_date"))
    state_purchase_end_date = _coerce_optional_iso_date(state.get("purchase_end_date"))
    state_catalog_query = str(state.get("catalog_query") or "").strip()
    if _is_generic_catalog_followup_query(state_catalog_query):
        state_catalog_query = ""
    previous_catalog_query = state_catalog_query or _latest_catalog_subject_query(conversation)
    previous_inventory_consumption_message = _latest_inventory_consumption_request_message(conversation)
    previous_estimation_context = bool(state.get("inventory_consumption_estimation_mode")) or _is_inventory_consumption_estimation_request(normalized_previous)
    estimation_followup = previous_estimation_context and (
        _is_short_followup_query(message)
        or _contains_normalized_word(normalized_current, "quanto", "quanta", "quanti", "quante")
        or "consum" in normalized_current
        or "giornal" in normalized_current
        or " al giorno" in f" {normalized_current}"
        or "non come giacenza" in normalized_current
    )
    if previous_estimation_context and current_inventory_query and (
        estimation_followup
    ):
        return [
            _build_inventory_consumption_estimation_tool_call(
                message,
                query_override=_extract_inventory_consumption_estimation_query(message) or current_inventory_query,
                thread_state=state,
            )
        ]

    if previous_inventory_consumption_message and _is_short_followup_query(message) and current_inventory_query:
        return [
            _build_inventory_consumption_tool_call(
                previous_inventory_consumption_message,
                query_override=current_inventory_query,
            )
        ]

    if _is_inventory_request(last_user_message, normalized_previous) and _is_short_followup_query(message) and current_inventory_query:
        return [_build_inventory_tool_call(message)]

    if _is_catalog_search_followup_request(normalized_current):
        catalog_query = previous_catalog_query
        if catalog_query:
            return [
                PlannedToolCall(
                    tool="search_products",
                    arguments={"query": catalog_query, "limit": _CHAT_LIST_LIMIT},
                )
            ]

    if previous_catalog_query and (
        _is_lowest_price_request(normalized_current)
        or _is_price_per_weight_request(normalized_current)
        or _is_missing_catalog_price_request(normalized_current)
        or _is_price_request(normalized_current)
    ):
        return [
            PlannedToolCall(
                tool="search_products",
                arguments={"query": previous_catalog_query, "limit": _CHAT_LIST_LIMIT},
            )
        ]

    if _is_purchase_time_request(normalized_current):
        inherited_query = current_query or state_purchase_query or previous_query
        if inherited_query:
            return [
                PlannedToolCall(
                    tool="get_purchase_frequency",
                    arguments={
                        "query": inherited_query,
                        "year": _extract_reference_year(message) or state_purchase_year,
                        "month": _extract_reference_month(message) or state_purchase_month,
                        "start_date": state_purchase_start_date,
                        "end_date": state_purchase_end_date,
                        "limit": _CHAT_LIST_LIMIT,
                    },
                )
            ]

    if _is_missing_price_variants_followup_request(normalized_current, conversation):
        state_query = str(state.get("purchase_query") or "").strip()
        purchase_overview_query = (
            ""
            if _is_generic_purchase_followup_query(state_query)
            else state_query
        ) or _latest_purchase_subject_query(conversation) or _latest_purchase_overview_query(conversation)
        if purchase_overview_query:
            return [
                PlannedToolCall(
                    tool="get_purchase_overview",
                    arguments={
                        "query": purchase_overview_query,
                        "year": state_purchase_year,
                        "month": state_purchase_month,
                        "start_date": state_purchase_start_date,
                        "end_date": state_purchase_end_date,
                        "limit": _CHAT_LIST_LIMIT,
                    },
                )
            ]

    if _is_purchase_expand_followup_request(normalized_current):
        purchase_overview_query = str(state.get("purchase_query") or "") or _latest_purchase_overview_query(conversation)
        if purchase_overview_query:
            return [
                PlannedToolCall(
                    tool="get_purchase_overview",
                    arguments={
                        "query": purchase_overview_query,
                        "limit": _CHAT_LIST_LIMIT,
                    },
                )
            ]

    if _is_purchase_followup_request(normalized_current) and any(keyword in normalized_previous for keyword in _ORDERS_KEYWORDS):
        year = _extract_reference_year(message) or _coerce_optional_year(state.get("purchase_year")) or _extract_reference_year(last_user_message)
        month = _extract_reference_month(message) or _coerce_optional_month(state.get("purchase_month")) or _extract_reference_month(last_user_message)
        previous_specific_query = "" if _is_generic_purchase_followup_query(previous_query) else previous_query
        current_specific_query = "" if _is_generic_purchase_followup_query(current_query) else current_query
        inherited_query = previous_specific_query if not current_specific_query else current_specific_query
        start_date = _coerce_optional_iso_date(state.get("purchase_start_date"))
        end_date = _coerce_optional_iso_date(state.get("purchase_end_date"))
        return [
            PlannedToolCall(
                tool="get_purchase_batches",
                arguments={
                    "query": inherited_query,
                    "year": year,
                    "month": month,
                    "start_date": start_date,
                    "end_date": end_date,
                    "limit": _CHAT_LIST_LIMIT,
                },
            )
        ]

    if not _is_short_followup_query(message):
        return []

    if not current_query:
        return []

    if "quante volte" in normalized_previous or "quanti ordini" in normalized_previous:
        return [
            PlannedToolCall(
                tool="get_purchase_frequency",
                arguments={"query": current_query, "year": year, "limit": _CHAT_LIST_LIMIT},
            )
        ]
    if _contains_normalized_word(normalized_previous, "quando"):
        return [
            PlannedToolCall(
                tool="get_purchase_history",
                arguments={"query": current_query, "year": year, "month": month, "limit": _CHAT_LIST_LIMIT},
            )
        ]
    if _is_catalog_request(last_user_message, normalized_previous, previous_query or current_query):
        search_query = current_query
        if previous_query and previous_query != current_query and (
            _is_price_request(normalized_current) or len(current_query.split()) <= 2
        ):
            search_query = f"{previous_query} {current_query}".strip()
        return [
            PlannedToolCall(
                tool="search_products",
                arguments={"query": search_query, "limit": _CHAT_LIST_LIMIT},
            )
        ]
    if _contains_normalized_word(normalized_previous, "marca", "marche"):
        return [
            PlannedToolCall(
                tool="get_purchase_overview",
                arguments={"query": current_query, "year": year, "limit": _CHAT_LIST_LIMIT},
            )
        ]
    return []


def _home_requires_direct_guardrail(
    message: str,
    normalized: str,
    direct_tool_calls: list[PlannedToolCall],
) -> bool:
    purchase_read_tools = {
        "get_purchase_overview",
        "compare_purchase_periods",
        "get_purchase_frequency",
        "get_purchase_batches",
        "get_purchase_history",
    }
    if direct_tool_calls and all(tool_call.tool in purchase_read_tools for tool_call in direct_tool_calls):
        return True
    if (
        _is_purchase_product_list_request(normalized)
        or _is_purchase_expand_followup_request(normalized)
        or _is_purchase_followup_request(normalized)
        or _is_purchase_time_request(normalized)
    ):
        return bool(direct_tool_calls)
    if any(tool_call.tool in {"list_tenant_users", "get_module_settings"} for tool_call in direct_tool_calls):
        return True
    if any(tool_call.tool in {"get_reservations_snapshot", "list_reservations"} for tool_call in direct_tool_calls):
        return True
    if any(tool_call.tool == "get_timeclock_summary" for tool_call in direct_tool_calls):
        return True
    if any(tool_call.tool == "get_inventory_consumption" for tool_call in direct_tool_calls):
        return True
    if any(tool_call.tool == "run_tenant_query" for tool_call in direct_tool_calls):
        return True
    if any(tool_call.tool == "search_products" for tool_call in direct_tool_calls) and (
        _is_catalog_search_followup_request(normalized)
        or _is_price_request(normalized)
        or _is_lowest_price_request(normalized)
        or _is_price_per_weight_request(normalized)
        or _is_missing_catalog_price_request(normalized)
    ):
        return True
    if _is_state_change_write_intent(message, normalized):
        return True
    if _is_purchase_batch_detail_request(message, normalized):
        return True
    return any(
        tool_call.tool
        in {
            "create_reservation",
            "update_reservation",
            "delete_reservation",
            "upsert_product",
            "write_shared_note",
            "write_sales_goal",
            "write_suspended_order",
        }
        for tool_call in direct_tool_calls
    )


def _home_requires_hard_direct_execution(
    message: str,
    normalized: str,
    direct_tool_calls: list[PlannedToolCall],
) -> bool:
    write_tools = {
        "create_reservation",
        "update_reservation",
        "delete_reservation",
        "create_google_workspace_document",
        "manage_tenant_user",
        "update_module_settings",
        "update_venue_profile",
        "upsert_product",
        "write_shared_note",
        "write_sales_goal",
        "write_suspended_order",
    }
    if _is_document_create_request(normalized) and _document_request_needs_grounded_data(message, normalized):
        return any(tool_call.tool in (write_tools - {"create_google_workspace_document"}) for tool_call in direct_tool_calls)
    if any(
        tool_call.tool == "run_tenant_query" and _sql_targets_homemade_stock(str(tool_call.arguments.get("sql") or ""))
        for tool_call in direct_tool_calls
    ):
        return True
    if any(tool_call.tool == "get_sales_goals" for tool_call in direct_tool_calls):
        return True
    if _is_state_change_write_intent(message, normalized):
        return True
    if any(tool_call.tool in write_tools for tool_call in direct_tool_calls):
        return True
    return False


def _should_prefer_planner_for_home(
    message: str,
    normalized: str,
    conversation: list[dict[str, str]],
    *,
    contextual_tool_calls: list[PlannedToolCall],
    direct_tool_calls: list[PlannedToolCall],
) -> bool:
    if _is_sql_analytics_request(normalized):
        return True
    if _home_requires_hard_direct_execution(message, normalized, direct_tool_calls):
        return False
    if contextual_tool_calls and all(tool_call.tool == "compare_purchase_periods" for tool_call in contextual_tool_calls):
        return False
    if _is_grounded_data_request(message, normalized):
        return True

    if any(keyword in normalized for keyword in _ORDERS_KEYWORDS):
        return True

    last_user = _latest_user_message(conversation)
    if last_user and any(keyword in _normalize_text(last_user) for keyword in _ORDERS_KEYWORDS):
        short_followup = len(normalized.split()) <= 8
        if short_followup:
            return True
    return False


def _is_reservation_create_request(normalized: str) -> bool:
    return "prenot" in normalized and _contains_normalized_word(
        normalized,
        "crea",
        "prenota",
        "inserisci",
        "aggiungi",
        "segna",
        "registra",
        "appunta",
    )


def _build_reservation_create_tool_call(message: str) -> PlannedToolCall:
    customer_name = _extract_reservation_customer_name(message)
    customer_phone = _extract_explicit_phone(message)
    customer_email = _extract_explicit_email(message)
    reservation_date = _extract_explicit_date(message)
    time_candidates = _extract_explicit_times(message)
    guests = _extract_explicit_guest_count(message)
    arguments: dict[str, object] = {
        "customer_name": customer_name,
        "customer_phone": customer_phone,
        "customer_email": customer_email,
        "reservation_date": reservation_date.isoformat() if reservation_date is not None else None,
        "start_time": _format_clock(time_candidates[0]) if len(time_candidates) == 1 else None,
        "guests": guests,
    }
    filtered_arguments = {key: value for key, value in arguments.items() if value is not None}
    return PlannedToolCall(tool="create_reservation", arguments=filtered_arguments)


def _build_reservation_create_clarification(message: str) -> str | None:
    normalized = _normalize_text(message)
    if not _is_reservation_create_request(normalized):
        return None

    time_candidates = _extract_explicit_times(message)
    if len(time_candidates) <= 1:
        return None

    rendered_times: list[str] = []
    for candidate in time_candidates:
        rendered = candidate.strftime("%H:%M")
        if rendered not in rendered_times:
            rendered_times.append(rendered)
    if len(rendered_times) <= 1:
        return None

    joined_times = " e ".join(rendered_times[:2]) if len(rendered_times) == 2 else ", ".join(rendered_times[:-1]) + f" e {rendered_times[-1]}"
    return f"Vedo piu orari nella richiesta: {joined_times}. Dimmi quale devo usare per la prenotazione."


def _parse_reservation_create_fragment(message: str) -> dict[str, object]:
    time_candidates = _extract_explicit_times(message)
    payload: dict[str, object] = {
        "customer_name": _extract_reservation_customer_name(message),
        "customer_phone": _extract_explicit_phone(message),
        "customer_email": _extract_explicit_email(message),
        "reservation_date": _extract_explicit_date(message),
        "guests": _extract_explicit_guest_count(message),
        "time_candidates": time_candidates,
    }
    if len(time_candidates) == 1:
        payload["start_time"] = time_candidates[0]
    return payload


def _conversation_suggests_pending_reservation_create(message: str, conversation: list[dict[str, str]]) -> bool:
    normalized = _normalize_text(message)
    if _is_reservation_create_request(normalized):
        return True

    last_user = _latest_user_message(conversation)
    if last_user and _is_reservation_create_request(_normalize_text(last_user)):
        return True

    for item in reversed(conversation[-6:]):
        if item.get("role") != "assistant":
            continue
        content = _normalize_text(str(item.get("content") or ""))
        if any(hint in content for hint in _RESERVATION_CREATE_ASSISTANT_HINTS):
            return True
    return False


def _build_contextual_reservation_create_tool_calls(message: str, conversation: list[dict[str, str]]) -> list[PlannedToolCall]:
    if not _conversation_suggests_pending_reservation_create(message, conversation):
        return []

    fragments: list[dict[str, object]] = []
    for item in conversation[-6:]:
        if item.get("role") != "user":
            continue
        fragments.append(_parse_reservation_create_fragment(str(item.get("content") or "")))
    current_fragment = _parse_reservation_create_fragment(message)
    fragments.append(current_fragment)

    if len(current_fragment.get("time_candidates") or []) > 1:
        return []

    merged: dict[str, object] = {}
    latest_single_time: time | None = None
    for fragment in fragments:
        for key in ("customer_name", "customer_phone", "customer_email", "reservation_date", "guests"):
            value = fragment.get(key)
            if value is not None:
                merged[key] = value
        if isinstance(fragment.get("start_time"), time):
            latest_single_time = fragment["start_time"]  # type: ignore[assignment]
    if latest_single_time is not None:
        merged["start_time"] = latest_single_time

    filtered_arguments: dict[str, object] = {}
    for key, value in merged.items():
        if isinstance(value, date):
            filtered_arguments[key] = value.isoformat()
        elif isinstance(value, time):
            filtered_arguments[key] = _format_clock(value)
        else:
            filtered_arguments[key] = value

    if not filtered_arguments:
        return []
    return [PlannedToolCall(tool="create_reservation", arguments=filtered_arguments)]


def _build_timeclock_summary_tool_call(message: str, normalized: str) -> PlannedToolCall:
    return PlannedToolCall(
        tool="get_timeclock_summary",
        arguments=_normalize_timeclock_tool_arguments(message, normalized, {"query_text": message}),
    )


def _build_tips_tool_call(
    message: str,
    *,
    fallback_area: str | None = None,
    fallback_year: int | None = None,
    fallback_month: int | None = None,
) -> PlannedToolCall:
    normalized = _normalize_text(message)
    tip_date = _extract_tips_date(message)
    tip_year = _extract_reference_year(message) or fallback_year
    tip_month = _extract_reference_month(message) or fallback_month
    area: str | None = None
    if "sala" in normalized:
        area = "sala"
    elif _contains_normalized_word(normalized, "bar"):
        area = "bar"
    elif fallback_area in {"sala", "bar"}:
        area = fallback_area

    conditions: list[str] = []
    if area:
        conditions.append(f"runs.area = '{_escape_sql_literal(area)}'")
    if tip_date is not None:
        conditions.append(f"runs.tip_date = '{tip_date.isoformat()}'")
    elif tip_year is not None and tip_month is not None:
        conditions.append(f"substr(runs.tip_date, 1, 7) = '{tip_year:04d}-{tip_month:02d}'")
    elif tip_year is not None:
        conditions.append(f"substr(runs.tip_date, 1, 4) = '{tip_year:04d}'")

    person_query = _extract_tips_query(message)
    staff_breakdown_requested = _is_tips_staff_breakdown_request(normalized) or _is_tips_report_request(normalized)
    person_tokens = [
        token
        for token in _tokenize_query(person_query)
        if token and token not in _TIPS_PERSON_QUERY_IGNORED_TOKENS
    ]
    if person_tokens:
        for token in person_tokens[:4]:
            escaped_token = _escape_sql_literal(token)
            conditions.append(
                "("
                f"lower(entries.staff_name) LIKE '%{escaped_token}%' "
                f"OR lower(entries.staff_lookup) LIKE '%{escaped_token}%'"
                ")"
            )
        where_sql = " AND ".join(conditions) if conditions else "1 = 1"
        sql = (
            "SELECT "
            "entries.staff_name AS staff_name, "
            "runs.area AS area, "
            "COUNT(DISTINCT runs.id) AS tip_days, "
            "ROUND(SUM(COALESCE(entries.amount_today, 0)), 2) AS total_assigned_amount, "
            "ROUND(SUM(COALESCE(entries.historical_amount, 0)), 2) AS total_loaded_history_amount, "
            "ROUND(SUM(COALESCE(entries.total_amount, 0)), 2) AS total_visible_amount "
            "FROM tips_run_entries AS entries "
            "JOIN tips_runs AS runs ON runs.id = entries.run_id "
            f"WHERE {where_sql} "
            "GROUP BY lower(entries.staff_lookup), entries.staff_name, runs.area "
            "ORDER BY total_visible_amount DESC, lower(entries.staff_name) ASC "
            f"LIMIT {_CHAT_LIST_LIMIT}"
        )
        return PlannedToolCall(tool="run_tenant_query", arguments={"sql": sql, "limit": _CHAT_LIST_LIMIT})

    ranking_requested = any(marker in normalized for marker in ("classifica", "graduatoria", "top", "primi")) or (
        "manc" in normalized and _contains_normalized_word(normalized, "chi", "piu", "più")
    )
    where_sql = " AND ".join(conditions) if conditions else "1 = 1"
    if _is_tips_total_request(normalized):
        sql = (
            "SELECT "
            "runs.area AS area, "
            "COUNT(DISTINCT runs.id) AS tip_days, "
            "ROUND(SUM(COALESCE(runs.total_tip_amount, 0)), 2) AS total_tip_amount_sum, "
            "ROUND(SUM(COALESCE(runs.tip_pos_amount, 0)), 2) AS total_pos_amount_sum, "
            "ROUND(SUM(COALESCE(runs.tip_pos_effective_amount, 0)), 2) AS total_pos_effective_amount_sum, "
            "ROUND(SUM(COALESCE(runs.historical_total_amount, 0)), 2) AS total_loaded_history_amount_sum, "
            "ROUND(SUM(COALESCE(runs.payable_total_amount, 0)), 2) AS total_payable_amount_sum "
            "FROM tips_runs AS runs "
            f"WHERE {where_sql} "
            "GROUP BY runs.area "
            "ORDER BY total_payable_amount_sum DESC, lower(runs.area) ASC "
            f"LIMIT {_CHAT_LIST_LIMIT}"
        )
        return PlannedToolCall(tool="run_tenant_query", arguments={"sql": sql, "limit": _CHAT_LIST_LIMIT})

    if ranking_requested or staff_breakdown_requested:
        sql = (
            "SELECT "
            "entries.staff_name AS staff_name, "
            "runs.area AS area, "
            "COUNT(DISTINCT runs.id) AS tip_days, "
            "ROUND(SUM(COALESCE(entries.amount_today, 0)), 2) AS total_assigned_amount, "
            "ROUND(SUM(COALESCE(entries.historical_amount, 0)), 2) AS total_loaded_history_amount, "
            "ROUND(SUM(COALESCE(entries.total_amount, 0)), 2) AS total_visible_amount "
            "FROM tips_run_entries AS entries "
            "JOIN tips_runs AS runs ON runs.id = entries.run_id "
            f"WHERE {where_sql} "
            "GROUP BY lower(entries.staff_lookup), entries.staff_name, runs.area "
            "ORDER BY total_visible_amount DESC, lower(entries.staff_name) ASC "
            f"LIMIT {_CHAT_LIST_LIMIT}"
        )
        return PlannedToolCall(tool="run_tenant_query", arguments={"sql": sql, "limit": _CHAT_LIST_LIMIT})

    sql = (
        "SELECT "
        "runs.tip_date AS tip_date, "
        "runs.area AS area, "
        "ROUND(runs.total_tip_amount, 2) AS total_tip_amount, "
        "ROUND(runs.tip_pos_amount, 2) AS tip_pos_amount, "
        "ROUND(runs.tip_pos_effective_amount, 2) AS tip_pos_effective_amount, "
        "ROUND(runs.historical_total_amount, 2) AS total_loaded_history_amount, "
        "ROUND(runs.payable_total_amount, 2) AS total_payable_amount, "
        "runs.present_staff_count AS present_staff_count "
        "FROM tips_runs AS runs "
        f"WHERE {where_sql} "
        "ORDER BY runs.tip_date DESC, runs.updated_at DESC "
        f"LIMIT {_CHAT_LIST_LIMIT}"
    )
    return PlannedToolCall(tool="run_tenant_query", arguments={"sql": sql, "limit": _CHAT_LIST_LIMIT})


def _build_inventory_consumption_tool_call(
    message: str,
    *,
    query_override: str | None = None,
    start_date: date | None = None,
    end_date: date | None = None,
) -> PlannedToolCall:
    query = (query_override or _extract_inventory_query(message)).strip()
    if start_date is None and end_date is None:
        explicit_date = _extract_explicit_date(message)
        if explicit_date is not None:
            end_date = explicit_date
        week_range = _extract_reference_week_range(message)
        if week_range is not None:
            start_date, end_date = week_range
    return PlannedToolCall(
        tool="get_inventory_consumption",
        arguments={
            "query": query,
            "start_date": start_date.isoformat() if start_date is not None else None,
            "end_date": end_date.isoformat() if end_date is not None else None,
            "limit": _CHAT_LIST_LIMIT,
        },
    )


def _build_inventory_tool_call(message: str) -> PlannedToolCall:
    normalized_message = _normalize_text(message)
    if _is_inventory_author_request(normalized_message):
        return _build_inventory_author_tool_call(message)

    if _is_inventory_ranking_request(normalized_message):
        result_limit = _extract_inventory_rank_limit(message)
        sql = (
            "SELECT "
            "1 AS inventory_rank_result, "
            "product_name, "
            "supplier_name, "
            "ROUND(SUM(total_equivalent_units), 3) AS total_equivalent_units, "
            "GROUP_CONCAT(warehouse_name || ': ' || RTRIM(RTRIM(printf('%.3f', total_equivalent_units), '0'), '.'), ' | ') AS warehouse_breakdown, "
            "GROUP_CONCAT(DISTINCT inventory_date) AS inventory_dates, "
            "GROUP_CONCAT(DISTINCT inventory_source) AS inventory_source "
            "FROM inventory_latest_items "
            "GROUP BY product_name, supplier_name "
            "ORDER BY total_equivalent_units DESC, lower(supplier_name) ASC, lower(product_name) ASC "
            f"LIMIT {result_limit}"
        )
        return PlannedToolCall(tool="run_tenant_query", arguments={"sql": sql, "limit": result_limit})

    inventory_query = _extract_inventory_query(message)
    threshold_match = _extract_inventory_total_threshold(message)
    if not inventory_query and threshold_match is None:
        sql = (
            "SELECT "
            "warehouse_name, "
            "latest_inventory_date, "
            "latest_inventory_created_by_name, "
            "latest_inventory_created_at, "
            "latest_inventory_total_products, "
            "latest_inventory_total_equivalent_units, "
            "product_count, "
            "current_total_equivalent_units, "
            "inventory_session_count "
            "FROM inventory_warehouses "
            "ORDER BY lower(warehouse_name) ASC "
            f"LIMIT {_CHAT_LIST_LIMIT}"
        )
        return PlannedToolCall(tool="run_tenant_query", arguments={"sql": sql, "limit": _CHAT_LIST_LIMIT})

    tokens = [
        token
        for token in _tokenize_query(inventory_query)
        if token and token not in _INVENTORY_QUERY_IGNORED_TOKENS
    ]
    threshold_value: float | None = None
    if threshold_match is not None:
        threshold_value, threshold_tokens = threshold_match
        tokens = [token for token in tokens if token not in threshold_tokens]
    if not tokens:
        tokens = [inventory_query] if threshold_value is None and inventory_query else []

    conditions: list[str] = []
    product_conditions: list[str] = []
    for token in tokens[:6]:
        escaped_token = _escape_sql_literal(token)
        conditions.append(
            "("
            f"lower(product_name) LIKE '%{escaped_token}%' "
            f"OR lower(supplier_name) LIKE '%{escaped_token}%' "
            f"OR lower(warehouse_name) LIKE '%{escaped_token}%'"
            ")"
        )
        product_conditions.append(
            "("
            f"lower(COALESCE(p.product_name, '')) LIKE '%{escaped_token}%' "
            f"OR lower(COALESCE(p.supplier_name, '')) LIKE '%{escaped_token}%' "
            f"OR lower(COALESCE(p.lot_code, '')) LIKE '%{escaped_token}%'"
            ")"
        )
    where_sql = " AND ".join(conditions) if conditions else "1 = 1"
    product_where_sql = " AND ".join(product_conditions) if product_conditions else "1 = 1"
    threshold_select_sql = ""
    having_sql = ""
    order_sql = "ORDER BY total_equivalent_units DESC, lower(supplier_name) ASC, lower(product_name) ASC "
    if threshold_value is not None:
        threshold_sql = _format_sql_numeric_literal(threshold_value)
        threshold_select_sql = f"{threshold_sql} AS threshold_units, "
        having_sql = f"HAVING SUM(total_equivalent_units) < {threshold_sql} "
        order_sql = "ORDER BY total_equivalent_units ASC, lower(supplier_name) ASC, lower(product_name) ASC "
    if conditions and threshold_value is None:
        sql = (
            "WITH catalog_matches AS ("
            "SELECT "
            "p.id AS product_id, "
            "p.product_name AS product_name, "
            "p.supplier_name AS supplier_name, "
            "GROUP_CONCAT(DISTINCT p.lot_code) AS catalog_lot_codes "
            "FROM ordini_products AS p "
            f"WHERE {product_where_sql} "
            "GROUP BY p.id, p.product_name, p.supplier_name"
            "), "
            "stock_totals AS ("
            "SELECT "
            "product_id, "
            "product_name, "
            "supplier_name, "
            "ROUND(SUM(total_equivalent_units), 3) AS total_equivalent_units, "
            "GROUP_CONCAT(warehouse_name || ': ' || RTRIM(RTRIM(printf('%.3f', total_equivalent_units), '0'), '.'), ' | ') AS warehouse_breakdown, "
            "GROUP_CONCAT(DISTINCT inventory_date) AS inventory_dates, "
            "GROUP_CONCAT(DISTINCT inventory_source) AS inventory_source, "
            "0 AS catalog_only, "
            "'' AS catalog_lot_codes "
            "FROM inventory_latest_items "
            f"WHERE ({where_sql}) "
            "OR product_id IN (SELECT product_id FROM catalog_matches WHERE product_id IS NOT NULL) "
            "GROUP BY product_id, product_name, supplier_name"
            "), "
            "combined AS ("
            "SELECT product_id, product_name, supplier_name, total_equivalent_units, warehouse_breakdown, inventory_dates, inventory_source, catalog_only, catalog_lot_codes "
            "FROM stock_totals "
            "UNION ALL "
            "SELECT "
            "c.product_id, "
            "c.product_name, "
            "c.supplier_name, "
            "0 AS total_equivalent_units, "
            "'' AS warehouse_breakdown, "
            "'' AS inventory_dates, "
            "'catalog_only' AS inventory_source, "
            "1 AS catalog_only, "
            "COALESCE(c.catalog_lot_codes, '') AS catalog_lot_codes "
            "FROM catalog_matches AS c "
            "WHERE NOT EXISTS ("
            "SELECT 1 FROM stock_totals AS s "
            "WHERE (s.product_id IS NOT NULL AND c.product_id IS NOT NULL AND s.product_id = c.product_id) "
            "OR (lower(s.product_name) = lower(c.product_name) AND lower(s.supplier_name) = lower(c.supplier_name))"
            ")"
            ") "
            "SELECT "
            "product_name, "
            "supplier_name, "
            "ROUND(SUM(total_equivalent_units), 3) AS total_equivalent_units, "
            "MAX(catalog_only) AS catalog_only, "
            "GROUP_CONCAT(DISTINCT NULLIF(catalog_lot_codes, '')) AS catalog_lot_codes, "
            "GROUP_CONCAT(NULLIF(warehouse_breakdown, ''), ' | ') AS warehouse_breakdown, "
            "GROUP_CONCAT(DISTINCT NULLIF(inventory_dates, '')) AS inventory_dates, "
            "GROUP_CONCAT(DISTINCT inventory_source) AS inventory_source "
            "FROM combined "
            "GROUP BY product_name, supplier_name "
            f"{order_sql}"
            f"LIMIT {_CHAT_LIST_LIMIT}"
        )
        return PlannedToolCall(tool="run_tenant_query", arguments={"sql": sql, "limit": _CHAT_LIST_LIMIT})
    sql = (
        "SELECT "
        "product_name, "
        "supplier_name, "
        "ROUND(SUM(total_equivalent_units), 3) AS total_equivalent_units, "
        f"{threshold_select_sql}"
        "GROUP_CONCAT(warehouse_name || ': ' || RTRIM(RTRIM(printf('%.3f', total_equivalent_units), '0'), '.'), ' | ') AS warehouse_breakdown, "
        "GROUP_CONCAT(DISTINCT inventory_date) AS inventory_dates, "
        "GROUP_CONCAT(DISTINCT inventory_source) AS inventory_source "
        "FROM inventory_latest_items "
        f"WHERE {where_sql} "
        "GROUP BY product_name, supplier_name "
        f"{having_sql}"
        f"{order_sql}"
        f"LIMIT {_CHAT_LIST_LIMIT}"
    )
    return PlannedToolCall(tool="run_tenant_query", arguments={"sql": sql, "limit": _CHAT_LIST_LIMIT})


def _build_fiscal_spend_tool_call(
    message: str,
    *,
    query_override: str | None = None,
    year_override: int | None = None,
    month_override: int | None = None,
    start_date_override: str | None = None,
    end_date_override: str | None = None,
) -> PlannedToolCall:
    normalized = _normalize_text(message)
    year = year_override if year_override is not None else _extract_reference_year(message)
    month = month_override if month_override is not None else _extract_reference_month(message)
    week_range = _extract_reference_week_range(message)
    query = (query_override or _extract_fiscal_spend_query(message)).strip()
    query_tokens = [
        token
        for token in _tokenize_query(query)
        if token
        and token not in _PURCHASE_QUERY_IGNORED_TOKENS
        and token not in {"fattura", "fatture", "bolla", "bolle", "ddt", "documento", "documenti", "fiscale", "fiscali"}
    ][:6]

    date_expr = "date(COALESCE(NULLIF(d.document_date, ''), substr(d.created_at, 1, 10)))"
    period_conditions: list[str] = []
    if week_range is not None:
        period_conditions.append(f"{date_expr} >= '{week_range[0].isoformat()}'")
        period_conditions.append(f"{date_expr} <= '{week_range[1].isoformat()}'")
    elif year is not None and month is not None:
        start_date, end_date = _month_date_range(year, month)
        period_conditions.append(f"{date_expr} >= '{start_date.isoformat()}'")
        period_conditions.append(f"{date_expr} <= '{end_date.isoformat()}'")
    elif year is not None:
        period_conditions.append(f"{date_expr} >= '{year:04d}-01-01'")
        period_conditions.append(f"{date_expr} <= '{year:04d}-12-31'")
    elif month is not None:
        today = _today_in_timezone()
        start_date, end_date = _month_date_range(today.year, month)
        period_conditions.append(f"{date_expr} >= '{start_date.isoformat()}'")
        period_conditions.append(f"{date_expr} <= '{end_date.isoformat()}'")

    document_conditions = [
        "d.status = 'ready'",
        "d.total_amount IS NOT NULL",
    ]
    document_conditions.extend(period_conditions)
    if start_date_override is not None and end_date_override is not None:
        period_conditions = [
            f"{date_expr} >= '{_escape_sql_literal(start_date_override)}'",
            f"{date_expr} <= '{_escape_sql_literal(end_date_override)}'",
        ]
        document_conditions = [
            "d.status = 'ready'",
            "d.total_amount IS NOT NULL",
        ]
        document_conditions.extend(period_conditions)
    document_where = " AND ".join(document_conditions)

    if query_tokens:
        document_token_conditions = []
        line_token_conditions = []
        for token in query_tokens:
            escaped = _escape_sql_literal(token)
            document_token_conditions.append(_fiscal_document_token_condition(token, escaped))
            line_token_conditions.append(_fiscal_line_token_condition(token, escaped))
        document_match_where = " AND ".join(document_token_conditions)
        line_match_where = " AND ".join(line_token_conditions)
    else:
        document_match_where = "1 = 1"
        line_match_where = "1 = 1"

    query_label = _escape_sql_literal(query)
    period_label = str(year or "").strip()
    if week_range is not None:
        period_label = f"{week_range[0].isoformat()} - {week_range[1].isoformat()}"
    elif year is not None and month is not None:
        start_date, end_date = _month_date_range(year, month)
        period_label = f"{start_date.isoformat()} - {end_date.isoformat()}"

    sql = (
        "WITH base_documents AS ("
        "SELECT d.* FROM tenant_fiscal_documents AS d "
        f"WHERE {document_where}"
        "), "
        "document_matches AS ("
        "SELECT * FROM base_documents AS d "
        f"WHERE {document_match_where}"
        "), "
        "line_matches AS ("
        "SELECT "
        "d.id AS document_id, "
        "d.display_name, "
        "d.document_type, "
        "d.document_date, "
        "d.supplier_name, "
        "i.description, "
        "i.line_total, "
        "i.vat_code, "
        "CASE "
        "WHEN i.line_total IS NULL THEN NULL "
        "ELSE i.line_total * (1 + COALESCE(NULLIF(CAST(i.vat_code AS REAL), 0), 22) / 100.0) "
        "END AS line_total_including_vat "
        "FROM base_documents AS d "
        "JOIN tenant_fiscal_document_items AS i ON i.document_id = d.id "
        f"WHERE {line_match_where}"
        ") "
        "SELECT "
        f"'{query_label}' AS query, "
        f"'{_escape_sql_literal(period_label)}' AS period, "
        "CASE WHEN (SELECT COUNT(*) FROM document_matches) > 0 THEN 'document_total_vat_included' ELSE 'line_total_vat_included_from_rows' END AS calculation_basis, "
        "CASE WHEN (SELECT COUNT(*) FROM document_matches) > 0 THEN (SELECT COUNT(*) FROM document_matches) ELSE (SELECT COUNT(DISTINCT document_id) FROM line_matches) END AS document_count, "
        "CASE WHEN (SELECT COUNT(*) FROM document_matches) > 0 THEN ROUND((SELECT SUM(total_amount) FROM document_matches), 2) ELSE ROUND((SELECT SUM(line_total_including_vat) FROM line_matches), 2) END AS total_amount_including_vat, "
        "CASE WHEN (SELECT COUNT(*) FROM document_matches) > 0 THEN GROUP_CONCAT(display_name, ' | ') ELSE (SELECT GROUP_CONCAT(DISTINCT display_name) FROM line_matches) END AS documents, "
        "CASE WHEN (SELECT COUNT(*) FROM document_matches) > 0 THEN GROUP_CONCAT(DISTINCT supplier_name) ELSE (SELECT GROUP_CONCAT(DISTINCT supplier_name) FROM line_matches) END AS suppliers "
        "FROM document_matches "
        "LIMIT 1"
    )
    return PlannedToolCall(tool="run_tenant_query", arguments={"sql": sql, "limit": 1})


def _build_homemade_tool_call(message: str, *, query_override: str | None = None) -> PlannedToolCall:
    raw_query = (query_override or _extract_homemade_query(message)).strip()
    query = raw_query if _is_meaningful_homemade_query(raw_query) else ""
    target_liters = _extract_liters_from_text(message)
    arguments: dict[str, object] = {}
    if query:
        arguments["query"] = query
    if target_liters is not None:
        arguments["target_liters"] = target_liters
    return PlannedToolCall(tool="get_homemade_recipe", arguments=arguments)


def _homemade_stock_consumption_period(message: str) -> tuple[str | None, str | None, str]:
    normalized = _normalize_text(message)
    today = _today_in_timezone()

    last_days_match = re.search(r"\bultim\w*\s+(\d{1,3})\s+giorn", normalized)
    if last_days_match:
        days = max(1, min(int(last_days_match.group(1)), 365))
        start_date = today - timedelta(days=days - 1)
        return start_date.isoformat(), (today + timedelta(days=1)).isoformat(), f"ultimi {days} giorni"

    explicit_date = _extract_explicit_date(message)
    if explicit_date is None and _contains_normalized_word(normalized, "ieri"):
        explicit_date = today - timedelta(days=1)
    if explicit_date is not None:
        return explicit_date.isoformat(), (explicit_date + timedelta(days=1)).isoformat(), explicit_date.isoformat()

    week_range = _extract_reference_week_range(message)
    if week_range is not None:
        start_date, end_exclusive = week_range
        return start_date.isoformat(), end_exclusive.isoformat(), f"dal {start_date.isoformat()} al {(end_exclusive - timedelta(days=1)).isoformat()}"

    if "settiman" in normalized:
        if any(fragment in normalized for fragment in ("settimana scorsa", "scorsa settimana", "settimana passata", "passata settimana")):
            start_date, end_date = _week_date_range(today - timedelta(days=7))
            return start_date.isoformat(), (end_date + timedelta(days=1)).isoformat(), "settimana scorsa"
        if any(fragment in normalized for fragment in ("settimana prossima", "prossima settimana", "settiman prossim")):
            start_date, end_date = _week_date_range(today + timedelta(days=7))
            return start_date.isoformat(), (end_date + timedelta(days=1)).isoformat(), "settimana prossima"
        start_date, end_date = _week_date_range(today, until_reference_day=True)
        return start_date.isoformat(), (end_date + timedelta(days=1)).isoformat(), "questa settimana"

    if "mese" in normalized:
        if any(fragment in normalized for fragment in ("mese scorso", "scorso mese", "mese passato", "passato mese")):
            year, month = _shift_year_month(today.year, today.month, -1)
            start_date, end_date = _month_date_range(year, month)
            return start_date.isoformat(), (end_date + timedelta(days=1)).isoformat(), "mese scorso"
        if any(fragment in normalized for fragment in ("mese prossimo", "prossimo mese", "mes prossim")):
            year, month = _shift_year_month(today.year, today.month, 1)
            start_date, end_date = _month_date_range(year, month)
            return start_date.isoformat(), (end_date + timedelta(days=1)).isoformat(), "mese prossimo"
        start_date, end_date = _month_date_range(today.year, today.month, until_today=True)
        return start_date.isoformat(), (end_date + timedelta(days=1)).isoformat(), "questo mese"

    month = _extract_reference_month(message)
    year = _extract_reference_year(message)
    if month is not None:
        resolved_year = year or today.year
        start_date, end_date = _month_date_range(resolved_year, month, until_today=resolved_year == today.year and month == today.month)
        return start_date.isoformat(), (end_date + timedelta(days=1)).isoformat(), f"{_format_italian_month(month)} {resolved_year}"

    if year is not None:
        end_date = today if year == today.year else date(year, 12, 31)
        return f"{year:04d}-01-01", (end_date + timedelta(days=1)).isoformat(), str(year)

    return None, None, "tutto lo storico disponibile"


def _homemade_stock_usage_scope_filter(message: str) -> tuple[str | None, str]:
    normalized = _normalize_text(message)
    if _contains_normalized_word(normalized, "ristorante", "terrazze", "sala"):
        return "restaurant", "ristorante"
    if _contains_normalized_word(normalized, "bar", "club"):
        return "bar", "bar/club"
    return None, ""


def _extract_homemade_stock_consumption_query(message: str) -> str:
    cleaned = re.sub(r"\bultim\w*\s+\d{1,3}\s+giorn\w*\b", " ", message, flags=re.IGNORECASE)
    cleaned = re.sub(r"\b20\d{2}\b", " ", cleaned, flags=re.IGNORECASE)
    cleaned = re.sub(r"\b\d{1,2}\s*[/-]\s*\d{1,2}(?:\s*[/-]\s*\d{2,4})?\b", " ", cleaned, flags=re.IGNORECASE)
    ignored_tokens = {
        "andamento",
        "andare",
        "andati",
        "andato",
        "bar",
        "club",
        "come",
        "consum",
        "consumare",
        "consumiamo",
        "consumi",
        "consumo",
        "consumata",
        "consumate",
        "consumati",
        "consumato",
        "cnsumi",
        "dammi",
        "dato",
        "dati",
        "dimmi",
        "elenca",
        "elencami",
        "fammi",
        "giorni",
        "giorno",
        "giornaliera",
        "giornaliere",
        "giornalieri",
        "giornaliero",
        "hai",
        "homemade",
        "indica",
        "indicami",
        "mese",
        "mostra",
        "mostrami",
        "nostra",
        "nostre",
        "nostri",
        "nostro",
        "prebatch",
        "prep",
        "preparazione",
        "preparazioni",
        "quest",
        "questa",
        "queste",
        "questi",
        "questo",
        "riepilogo",
        "ristorante",
        "scarichi",
        "scarico",
        "sala",
        "settimana",
        "stock",
        "sui",
        "sulle",
        "terrazze",
        "tutta",
        "tutte",
        "tutti",
        "tutto",
        "trovi",
        "trovami",
        "ultimi",
        "ultimo",
        "vanno",
        "va",
    }
    ignored_tokens.update(_ITALIAN_MONTHS.keys())
    ignored_prefixes = (
        "consum",
        "cnsum",
        "giornal",
        "indic",
        "mostr",
        "elenc",
        "scaric",
        "trov",
    )
    tokens = [
        token
        for token in _tokenize_query(cleaned)
        if token
        and not token.isdigit()
        and token not in ignored_tokens
        and not any(token.startswith(prefix) for prefix in ignored_prefixes)
        and not re.fullmatch(r"\d{1,3}", token)
    ]
    return " ".join(tokens[:6]).strip()


def _build_homemade_stock_consumption_tool_call(message: str, *, query_override: str | None = None) -> PlannedToolCall:
    query = (query_override if query_override is not None else _extract_homemade_stock_consumption_query(message)).strip()
    start_date, end_exclusive, period_label = _homemade_stock_consumption_period(message)
    usage_scope, usage_scope_label = _homemade_stock_usage_scope_filter(message)
    completed_days_end_date = (_today_in_timezone() - timedelta(days=1)).isoformat()
    calculation_start_sql = f"'{_escape_sql_literal(start_date)}'" if start_date else "m.first_observed_date"
    calculation_end_sql = (
        f"date('{_escape_sql_literal(end_exclusive)}', '-1 day')"
        if end_exclusive
        else f"'{_escape_sql_literal(completed_days_end_date)}'"
    )
    calculation_scope_sql = (
        f"'{_escape_sql_literal(usage_scope)}'"
        if usage_scope
        else "COALESCE(NULLIF(recipes.usage_scope, ''), 'both')"
    )

    conditions = ["(COALESCE(movements.consumed_quantity, 0) > 0 OR COALESCE(movements.added_quantity, 0) > 0)"]
    if start_date:
        conditions.append(f"substr(movements.occurred_at, 1, 10) >= '{_escape_sql_literal(start_date)}'")
    if end_exclusive:
        conditions.append(f"substr(movements.occurred_at, 1, 10) < '{_escape_sql_literal(end_exclusive)}'")
    if usage_scope:
        escaped_scope = _escape_sql_literal(usage_scope)
        conditions.append(f"COALESCE(NULLIF(recipes.usage_scope, ''), 'both') IN ('{escaped_scope}', 'both')")
    for token in _tokenize_query(query)[:6]:
        escaped = _escape_sql_literal(token)
        conditions.append(
            "("
            f"lower(COALESCE(movements.recipe_name, '')) LIKE '%{escaped}%' "
            f"OR lower(COALESCE(movements.recipe_lookup, '')) LIKE '%{escaped}%'"
            ")"
        )
    where_sql = " AND ".join(conditions)
    effective_period_label = f"{period_label}, area {usage_scope_label}" if usage_scope_label else period_label
    period_sql = _escape_sql_literal(effective_period_label)
    query_sql = _escape_sql_literal(query)
    sql = (
        "WITH movement_summary AS ("
        "SELECT movements.recipe_lookup, movements.recipe_name, movements.measurement_unit, "
        "COALESCE(NULLIF(recipes.usage_scope, ''), 'both') AS usage_scope, "
        f"{calculation_scope_sql} AS calculation_scope, "
        "COUNT(CASE WHEN COALESCE(movements.consumed_quantity, 0) > 0 THEN 1 END) AS movement_count, "
        "COUNT(DISTINCT CASE WHEN COALESCE(movements.consumed_quantity, 0) > 0 THEN substr(movements.occurred_at, 1, 10) END) AS consumption_event_days_count, "
        "MIN(substr(movements.occurred_at, 1, 10)) AS first_observed_date, "
        "MAX(substr(movements.occurred_at, 1, 10)) AS last_observed_date, "
        "MIN(CASE WHEN COALESCE(movements.consumed_quantity, 0) > 0 THEN substr(movements.occurred_at, 1, 10) END) AS first_consumed_date, "
        "MAX(CASE WHEN COALESCE(movements.consumed_quantity, 0) > 0 THEN substr(movements.occurred_at, 1, 10) END) AS last_consumed_date, "
        "ROUND(SUM(COALESCE(movements.consumed_quantity, 0)), 2) AS consumed_quantity, "
        "ROUND(SUM(COALESCE(movements.added_quantity, 0)), 2) AS added_quantity "
        "FROM tenant_homemade_stock_movements AS movements "
        "LEFT JOIN tenant_homemade_recipes AS recipes ON recipes.id = movements.recipe_id "
        f"WHERE {where_sql} "
        "GROUP BY movements.recipe_lookup, movements.recipe_name, movements.measurement_unit, usage_scope, calculation_scope "
        "HAVING consumed_quantity > 0"
        "), current_stock AS ("
        "SELECT recipe_lookup, ROUND(SUM(COALESCE(quantity, 0)), 2) AS current_quantity "
        "FROM tenant_homemade_stock_items "
        "GROUP BY recipe_lookup"
        "), calendar_counts AS ("
        "SELECT m.recipe_lookup, COUNT(DISTINCT days.operational_date) AS operational_days_count "
        "FROM movement_summary AS m "
        "JOIN tenant_homemade_operational_days AS days ON ("
        "(m.calculation_scope = 'bar' AND days.usage_scope = 'bar') "
        "OR (m.calculation_scope = 'restaurant' AND days.usage_scope = 'restaurant') "
        "OR (m.calculation_scope = 'both' AND days.usage_scope IN ('bar', 'restaurant'))"
        ") "
        f"AND days.operational_date >= {calculation_start_sql} "
        f"AND days.operational_date <= {calculation_end_sql} "
        "GROUP BY m.recipe_lookup"
        "), final_summary AS ("
        "SELECT m.*, "
        f"{calculation_start_sql} AS calculation_start_date, "
        f"{calculation_end_sql} AS calculation_end_date, "
        "COALESCE(cc.operational_days_count, 0) AS operational_days_count, "
        "CASE "
        "WHEN COALESCE(cc.operational_days_count, 0) > 0 THEN MAX(COALESCE(cc.operational_days_count, 0), m.consumption_event_days_count) "
        "ELSE m.consumption_event_days_count "
        "END AS workdays_count, "
        "CASE WHEN COALESCE(cc.operational_days_count, 0) > 0 THEN 'operational_calendar' ELSE 'movement_days' END AS calculation_basis, "
        "COALESCE(c.current_quantity, 0) AS current_quantity "
        "FROM movement_summary AS m "
        "LEFT JOIN current_stock AS c ON c.recipe_lookup = m.recipe_lookup "
        "LEFT JOIN calendar_counts AS cc ON cc.recipe_lookup = m.recipe_lookup "
        ") "
        "SELECT 1 AS homemade_consumption_result, "
        f"'{period_sql}' AS period_label, "
        f"'{query_sql}' AS query, "
        "m.recipe_name, m.usage_scope, m.calculation_scope, m.measurement_unit, "
        "m.consumed_quantity, m.added_quantity, m.workdays_count, m.consumption_event_days_count, m.operational_days_count, m.calculation_basis, "
        "ROUND(CASE WHEN m.workdays_count > 0 THEN m.consumed_quantity / m.workdays_count ELSE 0 END, 2) AS average_daily_consumption, "
        "m.current_quantity, "
        "ROUND(CASE WHEN m.consumed_quantity > 0 AND m.workdays_count > 0 THEN m.current_quantity / (m.consumed_quantity / m.workdays_count) ELSE NULL END, 2) AS coverage_days, "
        "m.first_observed_date, m.last_observed_date, m.first_consumed_date, m.last_consumed_date, "
        "m.calculation_start_date, m.calculation_end_date "
        "FROM final_summary AS m "
        "ORDER BY m.consumed_quantity DESC, lower(m.recipe_name) ASC "
        f"LIMIT {_CHAT_LIST_LIMIT}"
    )
    return PlannedToolCall(tool="run_tenant_query", arguments={"sql": sql, "limit": _CHAT_LIST_LIMIT})


def _supplier_catalog_rum_candidate_condition(alias: str = "sci") -> str:
    searchable = f"lower(' ' || COALESCE({alias}.source_name, '') || ' ')"
    category = f"upper(COALESCE({alias}.category, ''))"
    return (
        "("
        f"{category} = 'RUM' "
        f"OR {searchable} LIKE '% rum %' "
        f"OR {searchable} LIKE 'rum %' "
        f"OR {searchable} LIKE '% rhum %'"
        ")"
    )


def _supplier_catalog_jamaican_rum_match_condition(alias: str = "sci") -> str:
    searchable = f"lower(COALESCE({alias}.source_name, '') || ' ' || COALESCE({alias}.category, ''))"
    checks = [
        f"{searchable} LIKE '%{_escape_sql_literal(token)}%'"
        for token, _reason in _SUPPLIER_CATALOG_JAMAICAN_RUM_RULES
    ]
    return "(" + " OR ".join(checks) + ")"


def _supplier_catalog_jamaican_rum_reason_case(alias: str = "sci") -> str:
    searchable = f"lower(COALESCE({alias}.source_name, '') || ' ' || COALESCE({alias}.category, ''))"
    cases = [
        f"WHEN {searchable} LIKE '%{_escape_sql_literal(token)}%' THEN '{_escape_sql_literal(reason)}'"
        for token, reason in _SUPPLIER_CATALOG_JAMAICAN_RUM_RULES
    ]
    return "CASE " + " ".join(cases) + " ELSE '' END"


def _extract_supplier_catalog_semantic_filter(message: str, query: str) -> dict[str, str] | None:
    normalized = _normalize_text(f"{message} {query}")
    wants_jamaican = _contains_normalized_word(
        normalized,
        "jamaica",
        "jamaican",
        "jamaicano",
        "jamaicana",
        "jamaicani",
        "jamaicane",
        "giamaica",
        "giamaicano",
        "giamaicana",
        "giamaicani",
        "giamaicane",
    )
    wants_rum = _contains_normalized_word(normalized, "rum", "rhum")
    if not wants_jamaican or not wants_rum:
        return None
    return {
        "subject": "rum",
        "attribute": "jamaicani",
        "candidate_condition": _supplier_catalog_rum_candidate_condition("sci"),
        "match_condition": _supplier_catalog_jamaican_rum_match_condition("sci"),
        "reason_case": _supplier_catalog_jamaican_rum_reason_case("sci"),
    }


def _build_supplier_catalog_lookup_tool_call(message: str) -> PlannedToolCall | None:
    supplier_query = _extract_supplier_catalog_supplier_query(message)
    query = _extract_supplier_catalog_product_query(message, supplier_query)
    if not query:
        query = (_extract_catalog_query(message) or _extract_product_query(message)).strip()
    semantic_filter = _extract_supplier_catalog_semantic_filter(message, query)
    if semantic_filter is not None:
        query = f"{semantic_filter['subject']} {semantic_filter['attribute']}".strip()
    query_tokens = _significant_catalog_query_tokens(query) or _catalog_query_tokens(query)
    if not query_tokens:
        return None

    def _token_condition(alias: str, columns: tuple[str, ...], token: str) -> str:
        column_checks = []
        for variant in _query_token_variants(token):
            escaped = _escape_sql_literal(variant)
            column_checks.extend(
                f"lower(COALESCE({alias}.{column}, '')) LIKE '%{escaped}%'"
                for column in columns
            )
        return "(" + " OR ".join(column_checks) + ")"

    supplier_token_conditions = [
        _token_condition("sc", ("supplier_name", "catalog_name", "source_file_name"), token)
        for token in (_catalog_query_tokens(supplier_query) if supplier_query else [])
    ]
    if semantic_filter is not None:
        supplier_conditions = [semantic_filter["candidate_condition"]]
    else:
        supplier_conditions = [
            _token_condition("sci", ("source_name", "source_lot_code", "product_code", "category"), token)
            for token in query_tokens
        ]
    local_conditions = [
        _token_condition("op", ("product_name", "lot_code", "supplier_name", "product_code"), token)
        for token in query_tokens
    ]
    lookup_query = _escape_sql_literal(query)
    supplier_where = " AND ".join(supplier_conditions)
    if supplier_token_conditions:
        supplier_where = f"{supplier_where} AND " + " AND ".join(supplier_token_conditions)
    local_where = " AND ".join(local_conditions)
    supplier_query_sql = _escape_sql_literal(supplier_query)
    semantic_select_columns = ""
    semantic_order_prefix = ""
    if semantic_filter is not None:
        semantic_select_columns = (
            f"'{_escape_sql_literal(semantic_filter['subject'])}' AS catalog_semantic_subject, "
            f"'{_escape_sql_literal(semantic_filter['attribute'])}' AS catalog_semantic_attribute, "
            f"CASE WHEN {semantic_filter['match_condition']} THEN 1 ELSE 0 END AS semantic_match, "
            f"{semantic_filter['reason_case']} AS classification_reason, "
        )
        semantic_order_prefix = "semantic_match DESC, "
    supplier_select = (
        "SELECT 0 AS sort_scope, 'cataloghi_fornitori' AS source_scope, "
        f"'{lookup_query}' AS lookup_query, '{supplier_query_sql}' AS lookup_supplier, "
        f"{semantic_select_columns}"
        "sci.source_name AS display_name, sci.source_lot_code AS display_lot_code, "
        "COALESCE(NULLIF(sci.source_supplier_name, ''), sc.supplier_name) AS display_supplier_name, "
        "sci.final_price_vat AS display_price_vat, "
        "sci.product_code AS display_product_code, sci.category AS display_category "
        "FROM supplier_catalog_items AS sci "
        "LEFT JOIN supplier_catalogs AS sc ON sc.id = sci.catalog_id "
        f"WHERE {supplier_where} "
    )
    if supplier_query or semantic_filter is not None:
        sql = supplier_select + f"ORDER BY {semantic_order_prefix}display_name ASC LIMIT " + str(_CHAT_LIST_LIMIT)
    else:
        sql = (
            supplier_select
            + "UNION ALL "
            "SELECT 1 AS sort_scope, 'catalogo_locale' AS source_scope, "
            f"'{lookup_query}' AS lookup_query, '' AS lookup_supplier, "
            "op.product_name AS display_name, op.lot_code AS display_lot_code, "
            "op.supplier_name AS display_supplier_name, op.final_price_vat AS display_price_vat, "
            "op.product_code AS display_product_code, NULL AS display_category "
            "FROM ordini_products AS op "
            f"WHERE op.active = 1 AND {local_where} "
            "ORDER BY sort_scope ASC, display_name ASC "
            f"LIMIT {_CHAT_LIST_LIMIT}"
        )
    return PlannedToolCall(tool="run_tenant_query", arguments={"sql": sql, "limit": _CHAT_LIST_LIMIT})


def _build_direct_tool_calls(message: str) -> list[PlannedToolCall]:
    normalized = _normalize_text(message)
    query = _extract_product_query(message)
    purchase_query = _extract_purchase_query(message)
    week_range = _extract_reference_week_range(message)
    chronological_order_rank = _extract_chronological_order_rank(message)
    if _is_timeclock_request(normalized):
        return [_build_timeclock_summary_tool_call(message, normalized)]
    if _is_tips_request(message, normalized):
        tips_tool_call = _build_tips_tool_call(message)
        if _is_document_create_request(normalized):
            return [tips_tool_call, _build_document_create_tool_call(message)]
        return [tips_tool_call]
    if _is_homemade_stock_consumption_request(normalized):
        return [_build_homemade_stock_consumption_tool_call(message)]
    if _is_inventory_consumption_estimation_request(normalized) and _has_explicit_inventory_estimation_scope(message, normalized):
        return [_build_inventory_consumption_estimation_tool_call(message)]
    if _is_inventory_consumption_request(message, normalized):
        return [_build_inventory_consumption_tool_call(message)]
    if _is_inventory_request(message, normalized):
        return [_build_inventory_tool_call(message)]
    if _is_fiscal_spend_request(message, normalized):
        return [_build_fiscal_spend_tool_call(message)]
    if _is_homemade_request(message, normalized):
        return [_build_homemade_tool_call(message)]
    if _is_purchase_comparison_request(message, normalized):
        if (
            _contains_normalized_word(normalized, "ultimo", "ultimi")
            and _contains_normalized_word(normalized, "ordine", "ordini")
        ):
            years = _extract_reference_years(message)
            if len(years) >= 2:
                return [
                    PlannedToolCall(tool="get_purchase_batches", arguments={"query": "", "year": years[0], "limit": 1}),
                    PlannedToolCall(tool="get_purchase_batches", arguments={"query": "", "year": years[1], "limit": 1}),
                ]
        percentage_requested = any(fragment in normalized for fragment in ("percentuale", "percent", "quanto percent"))
        comparison_tool_call = _build_purchase_comparison_tool_call(message, percentage_requested=percentage_requested)
        if comparison_tool_call is not None:
            return [comparison_tool_call]

    if _is_tenant_user_list_request(normalized):
        if _is_document_create_request(normalized):
            return [PlannedToolCall(tool="list_tenant_users", arguments={}), _build_document_create_tool_call(message)]
        return [PlannedToolCall(tool="list_tenant_users", arguments={})]

    if _is_document_create_request(normalized):
        if _document_request_needs_grounded_data(message, normalized):
            return _build_grounded_document_tool_calls(message)
        return [_build_document_create_tool_call(message)]

    if _is_sql_analytics_request(normalized):
        return _build_sql_analytics_tool_calls(message, normalized)

    if _is_first_batches_request(normalized) or chronological_order_rank is not None:
        year = _extract_reference_year(message)
        month = _extract_reference_month(message)
        return [
            PlannedToolCall(
                tool="get_purchase_batches",
                arguments={
                    "query": purchase_query,
                    "year": year,
                    "month": month,
                    "start_date": week_range[0].isoformat() if week_range is not None else None,
                    "end_date": week_range[1].isoformat() if week_range is not None else None,
                    "sort_order": "earliest",
                    "limit": chronological_order_rank or 1,
                },
            )
        ]

    if _is_latest_batches_request(normalized):
        year = _extract_reference_year(message)
        month = _extract_reference_month(message)
        limit = _extract_requested_latest_batches_limit(message)
        return [
            PlannedToolCall(
                tool="get_purchase_batches",
                arguments={
                    "query": purchase_query,
                    "year": year,
                    "month": month,
                    "limit": limit,
                },
            )
        ]

    if _is_reservation_create_request(normalized):
        return [_build_reservation_create_tool_call(message)]
    if _is_reservation_write_request(normalized):
        return []
    if _is_product_write_request(message, normalized):
        return [_build_product_write_tool_call(message)]
    if _is_sales_goal_write_request(normalized):
        goal_arguments = _parse_sales_goal_write_fragment(message)
        meaningful_fields = {
            "goal_id",
            "name",
            "goal_type",
            "description",
            "product_match",
            "secondary_product_match",
            "supplier_match",
            "target",
            "secondary_target",
        }
        if any(key in goal_arguments for key in meaningful_fields):
            return [PlannedToolCall(tool="write_sales_goal", arguments=goal_arguments)]
        return []

    if _SUSPENDED_ORDER_PATTERN.search(message):
        operation: Literal["set", "add"] = "add" if any(word in normalized for word in ("aggiungi", "aggiungere", "metti", "inserisci")) else "set"
        quantity = _extract_requested_quantity(message)
        product_match = re.search(r"(?:\bdi\b|\bcon\b)\s+(.+)$", message, re.IGNORECASE)
        product_query = product_match.group(1).strip(" .,!?:;") if product_match else _extract_product_query(message)
        if product_query:
            return [
                PlannedToolCall(
                    tool="write_suspended_order",
                    arguments={
                        "operation": operation,
                        "items": [{"product_query": product_query, "quantity": quantity}],
                    },
                )
            ]

    if _contains_normalized_word(normalized, "nota", "note"):
        if any(keyword in normalized for keyword in ("mostra", "mostrami", "lista", "elenca", "vedere", "leggi", "quali")):
            return [PlannedToolCall(tool="list_shared_notes", arguments={"limit": _CHAT_LIST_LIMIT})]
        if any(keyword in normalized for keyword in ("aggiungi", "scrivi", "salva", "crea", "aggiorna", "modifica", "elimina", "cancella")):
            note_text = _extract_note_text(message)
            if note_text:
                return [PlannedToolCall(tool="write_shared_note", arguments={"operation": "create", "text": note_text})]

    module_settings_target = _extract_module_settings_target(normalized)
    if module_settings_target and _is_module_settings_read_request(normalized):
        return [PlannedToolCall(tool="get_module_settings", arguments={"module": module_settings_target})]

    if _is_sales_goal_read_request(normalized):
        year = _extract_explicit_year(message)
        return [PlannedToolCall(tool="get_sales_goals", arguments={"year": year})]

    if _is_supplier_catalog_request(normalized):
        supplier_catalog_tool_call = _build_supplier_catalog_lookup_tool_call(message)
        if supplier_catalog_tool_call is not None:
            return [supplier_catalog_tool_call]

    if any(keyword in normalized for keyword in _RESERVATION_KEYWORDS):
        offset_days = 0
        if "domani" in normalized:
            offset_days = 1
        elif "ieri" in normalized:
            offset_days = -1
        target_date = (_today_in_timezone() + timedelta(days=offset_days)).isoformat()
        explicit_times = _extract_explicit_times(message)
        time_window = "evening" if "stasera" in normalized or "sera" in normalized else "lunch" if "pranzo" in normalized else "all_day"
        return [
            PlannedToolCall(
                tool="get_reservations_snapshot",
                arguments={
                    "target_date": target_date,
                    "target_time": explicit_times[0].strftime("%H:%M") if explicit_times else None,
                    "time_window": time_window,
                    "limit": _CHAT_LIST_LIMIT,
                },
            )
        ]

    if _is_purchase_product_list_request(normalized):
        year = _extract_reference_year(message)
        month = _extract_reference_month(message)
        return [
            PlannedToolCall(
                tool="get_purchase_overview",
                arguments={
                    "query": purchase_query,
                    "year": year,
                    "month": month,
                    "limit": _CHAT_LIST_LIMIT,
                },
            )
        ]

    if _is_catalog_data_request(normalized):
        detail_query = _extract_catalog_subject_query(message) or query
        if detail_query:
            return [
                PlannedToolCall(
                    tool="search_products",
                    arguments={
                        "query": detail_query,
                        "limit": _CHAT_LIST_LIMIT,
                    },
                )
            ]

    if _is_catalog_request(message, normalized, query):
        return [
            PlannedToolCall(
                tool="search_products",
                arguments={
                    "query": query,
                    "limit": _CHAT_LIST_LIMIT,
                },
            )
        ]

    if any(keyword in normalized for keyword in _ORDERS_KEYWORDS):
        year = _extract_reference_year(message) or (week_range[0].year if week_range is not None else None)
        month = _extract_reference_month(message) or (week_range[0].month if week_range is not None else None)
        start_date = week_range[0].isoformat() if week_range is not None else None
        end_date = week_range[1].isoformat() if week_range is not None else None
        if _is_purchase_batch_detail_request(message, normalized):
            batch_id = _extract_purchase_batch_id(message)
            target_date = _extract_purchase_batch_date(message)
            return [
                PlannedToolCall(
                    tool="get_purchase_batches",
                    arguments={
                        "query": "",
                        "batch_id": batch_id,
                        "target_date": None if batch_id is not None else (target_date.isoformat() if isinstance(target_date, date) else None),
                        "year": None if batch_id is not None or target_date is not None else year,
                        "month": None if batch_id is not None or target_date is not None else month,
                        "start_date": None if batch_id is not None or target_date is not None else start_date,
                        "end_date": None if batch_id is not None or target_date is not None else end_date,
                        "limit": _CHAT_LIST_LIMIT if target_date is not None and batch_id is None else 1,
                    },
                )
            ]
        if "quante volte" in normalized or "quanti ordini" in normalized:
            return [
                PlannedToolCall(
                    tool="get_purchase_frequency",
                    arguments={
                        "query": purchase_query,
                        "year": year,
                        "month": month,
                        "start_date": start_date,
                        "end_date": end_date,
                        "limit": _CHAT_LIST_LIMIT,
                    },
                )
            ]
        if _is_purchase_time_request(normalized):
            return [
                PlannedToolCall(
                    tool="get_purchase_frequency",
                    arguments={
                        "query": purchase_query,
                        "year": year,
                        "month": month,
                        "start_date": start_date,
                        "end_date": end_date,
                        "limit": _CHAT_LIST_LIMIT,
                    },
                )
            ]
        if (
            any(keyword in normalized for keyword in ("mostra", "mostrami", "elenca", "elencami", "lista", "vedere", "vedi"))
            and _contains_normalized_word(normalized, "ordine", "ordini")
            and not _is_purchase_product_list_request(normalized)
        ):
            return [
                PlannedToolCall(
                    tool="get_purchase_batches",
                    arguments={
                        "query": purchase_query,
                        "year": year,
                        "month": month,
                        "start_date": start_date,
                        "end_date": end_date,
                        "limit": _CHAT_LIST_LIMIT,
                    },
                )
            ]
        if _is_purchase_history_request(normalized):
            return [
                PlannedToolCall(
                    tool="get_purchase_history",
                    arguments={
                        "query": purchase_query,
                        "year": year,
                        "month": month,
                        "start_date": start_date,
                        "end_date": end_date,
                        "limit": _CHAT_LIST_LIMIT,
                    },
                )
            ]
        if _is_purchase_product_list_request(normalized):
            return [
                PlannedToolCall(
                    tool="get_purchase_overview",
                    arguments={
                        "query": purchase_query,
                        "year": year,
                        "month": month,
                        "start_date": start_date,
                        "end_date": end_date,
                        "limit": _CHAT_LIST_LIMIT,
                    },
                )
            ]
        tool_name = "get_purchase_history" if any(keyword in normalized for keyword in ("storico", "ultimo", "ultimi", "quando")) else "get_purchase_overview"
        return [
            PlannedToolCall(
                tool=tool_name,
                arguments={
                    "query": purchase_query,
                    "year": year,
                    "month": month,
                    "start_date": start_date,
                    "end_date": end_date,
                    "limit": _CHAT_LIST_LIMIT,
                },
            )
        ]

    return []


async def _plan_tool_usage(
    session: SessionIdentity,
    *,
    message: str,
    conversation: list[dict[str, str]],
    thread_state: dict[str, object] | None = None,
) -> AssistantPlan:
    thread_state_summary = _build_home_thread_state_summary(thread_state)
    planner_messages = [
        {"role": "system", "content": _planner_system_prompt(session)},
        {
            "role": "user",
            "content": "\n".join(
                [
                    "Contesto runtime:",
                    _planner_runtime_context(session),
                    "",
                    "Stato strutturato del thread:",
                    thread_state_summary,
                    "",
                    "Conversazione recente:",
                    _build_recent_conversation(conversation),
                    "",
                    f"Messaggio utente attuale: {message.strip()}",
                ]
            ),
        },
    ]

    reply, _ = await request_llm_chat_completion(
        planner_messages,
        temperature=0.0,
        max_tokens=get_settings().assistant_max_tokens,
        model=_assistant_planner_model(),
    )
    try:
        parsed = _normalize_plan_payload(_parse_json_object(reply))
        return AssistantPlan.model_validate(parsed)
    except (ValueError, ValidationError):
        repair_messages = [
            {
                "role": "system",
                "content": (
                    "Ripari l'output di un planner. "
                    "Devi restituire SOLO JSON valido nel formato "
                    '{"mode":"reply","reply":"...","tool_calls":[]} oppure '
                    '{"mode":"tool","reply":null,"tool_calls":[{"tool":"nome_tool","arguments":{...}}]}. '
                    "Non cambiare l'intento dell'utente. Se l'output originale contiene gia un tool o i suoi argomenti, "
                    "wrappalo correttamente senza aggiungere testo fuori schema."
                ),
            },
            {
                "role": "user",
                "content": "\n".join(
                    [
                        "Contesto runtime:",
                        _planner_runtime_context(session),
                        "",
                        "Stato strutturato del thread:",
                        thread_state_summary,
                        "",
                        "Conversazione recente:",
                        _build_recent_conversation(conversation),
                        "",
                        f"Messaggio utente attuale: {message.strip()}",
                        "",
                        "Output planner originale da riparare:",
                        reply.strip() or "(vuoto)",
                    ]
                ),
            },
        ]
        repaired_reply, _ = await request_llm_chat_completion(
            repair_messages,
            temperature=0.0,
            max_tokens=get_settings().assistant_max_tokens,
            model=_assistant_planner_model(),
        )
        repaired = _normalize_plan_payload(_parse_json_object(repaired_reply))
        return AssistantPlan.model_validate(repaired)


def _render_reservation_candidate_lines(candidates: list[dict[str, object]]) -> str:
    lines = []
    for candidate in candidates:
        phone = str(candidate.get("customer_phone") or "").strip()
        suffix = f" ({phone})" if phone else ""
        lines.append(
            f"- {candidate.get('customer_name')} il {candidate.get('reservation_date')} alle {candidate.get('start_time')} per {candidate.get('guests')} persone{suffix}"
        )
    return "\n".join(lines)


def _render_purchase_batch(batch: dict[str, object]) -> list[str]:
    confirmed_at = str(batch.get("confirmed_at") or "").replace("T", " ")
    total_lines = int(batch.get("total_lines") or 0)
    total_quantity = int(batch.get("total_quantity") or 0)
    total_estimated_amount = _coerce_positive_float(batch.get("total_estimated_amount"))
    pricing_basis = str(batch.get("pricing_basis") or "")
    missing_price_lines = int(batch.get("missing_price_lines") or 0)
    suppliers = batch.get("supplier_names") if isinstance(batch.get("supplier_names"), list) else []
    supplier_summary = ", ".join(str(item) for item in suppliers) if suppliers else "n/d"
    lines = [
        f"Ordine #{batch.get('batch_id')} del {confirmed_at}, inserito da {batch.get('staff')}.",
        f"Righe: {total_lines}. Quantita totale: {total_quantity}. Fornitori coinvolti: {supplier_summary}.",
    ]
    if total_estimated_amount is not None:
        if pricing_basis == "order_snapshot":
            lines.append(f"Totale ordine: {_format_eur(total_estimated_amount)} (prezzo snapshot salvato).")
        else:
            lines.append(f"Totale ordine stimato: {_format_eur(total_estimated_amount)}.")
    if missing_price_lines:
        lines.append(f"Prezzi mancanti non inclusi nel totale: {missing_price_lines} righe.")
    items = batch.get("items") if isinstance(batch.get("items"), list) else []
    if items:
        lines.append("Contenuto ordine:")
        for item in items:
            if isinstance(item, dict):
                line_total = _coerce_positive_float(item.get("estimated_line_total"))
                suffix = f" · {_format_eur(line_total)}" if line_total is not None else ""
                lines.append(
                    f"- {item.get('quantity')} x {item.get('product_name')} ({item.get('lot_code')}) da {item.get('supplier_name')}{suffix}"
                )
    return lines


def _render_purchase_batch_matched_quantity(message: str, batch: dict[str, object], query: str) -> str | None:
    if not query.strip():
        return None
    normalized = _normalize_text(message)
    if not (
        _contains_normalized_word(normalized, "quanto", "quanta", "quanti", "quante", "quantita")
        or "di quanti" in normalized
    ):
        return None
    matched_items = batch.get("matched_items") if isinstance(batch.get("matched_items"), list) else []
    if not matched_items:
        return None

    confirmed_at = str(batch.get("confirmed_at") or "").replace("T", " ")
    matched_total_quantity = int(batch.get("matched_total_quantity") or 0)
    if matched_total_quantity <= 0:
        matched_total_quantity = sum(int(item.get("quantity") or 0) for item in matched_items if isinstance(item, dict))

    prefix = "La prima volta che lo hai ordinato" if _is_first_batches_request(normalized) else "Nell'ordine richiesto"
    if len(matched_items) == 1 and isinstance(matched_items[0], dict):
        item = matched_items[0]
        lot_code = str(item.get("lot_code") or "").strip()
        lot_suffix = f" {lot_code}" if lot_code else ""
        return (
            f"{prefix}, hai comprato {int(item.get('quantity') or 0)}{lot_suffix} di {item.get('product_name')} "
            f"da {item.get('supplier_name')} nell'ordine #{batch.get('batch_id')} del {confirmed_at}."
        )

    lines = [
        f"{prefix}, le righe compatibili con {query} sono {matched_total_quantity} unita nell'ordine #{batch.get('batch_id')} del {confirmed_at}:"
    ]
    for item in matched_items:
        if isinstance(item, dict):
            lot_code = str(item.get("lot_code") or "").strip()
            lot_suffix = f" {lot_code}" if lot_code else ""
            lines.append(f"- {int(item.get('quantity') or 0)}{lot_suffix} di {item.get('product_name')} da {item.get('supplier_name')}")
    return "\n".join(lines)


def _is_capacity_question(normalized_message: str) -> bool:
    capacity_keywords = ("copert", "capienz", "posti", "posto", "tutto il locale", "in tutto", "totale")
    return any(keyword in normalized_message for keyword in capacity_keywords)


def _is_reservation_subject_request(normalized_message: str) -> bool:
    return any(keyword in normalized_message for keyword in _RESERVATION_SUBJECT_KEYWORDS)


def _sql_targets_reservations(sql: str) -> bool:
    normalized_sql = _normalize_text(sql)
    return "tenant_reservations" in normalized_sql or re.search(r"\bfrom\s+reservations\b", normalized_sql) is not None


def _sql_targets_timeclock(sql: str) -> bool:
    normalized_sql = _normalize_text(sql)
    return "timeclock" in normalized_sql or "tenant_timeclock_entries" in normalized_sql


def _sql_targets_inventory(sql: str) -> bool:
    normalized_sql = _normalize_text(sql)
    return any(
        fragment in normalized_sql
        for fragment in (
            "inventory latest items",
            "inventory latest lots",
            "inventory warehouses",
            "tenant inventory",
        )
    )


def _sql_targets_homemade_stock(sql: str) -> bool:
    normalized_sql = _normalize_text(sql)
    return any(
        fragment in normalized_sql
        for fragment in (
            "tenant homemade stock",
            "homemade stock",
            "prep stock",
            "homemade consumption result",
        )
    )


def _sql_targets_tips(sql: str) -> bool:
    normalized_sql = _normalize_text(sql)
    return any(
        fragment in normalized_sql
        for fragment in (
            "tips runs",
            "tips run entries",
            "tips roster",
            "tenant tips",
            "mance runs",
            "mance entries",
        )
    )


def _sql_targets_fiscal_documents(sql: str) -> bool:
    normalized_sql = _normalize_text(sql)
    return "tenant fiscal documents" in normalized_sql or "tenant fiscal document items" in normalized_sql


def _sql_is_fiscal_spend_query(sql: str) -> bool:
    normalized_sql = _normalize_text(sql)
    return _sql_targets_fiscal_documents(sql) and "total amount including vat" in normalized_sql


def _extract_sql_alias_literal(sql: str, alias: str) -> str:
    match = re.search(rf"'([^']*)'\s+AS\s+{re.escape(alias)}\b", sql, flags=re.IGNORECASE)
    return match.group(1).strip() if match else ""


def _sql_targets_supplier_catalog(sql: str) -> bool:
    lowered = sql.lower()
    normalized_sql = _normalize_text(sql)
    return (
        "supplier_catalog_items" in lowered
        or "fornitori_cataloghi_items" in lowered
        or "supplier catalog items" in normalized_sql
        or "fornitori cataloghi items" in normalized_sql
    )


def _extract_supplier_catalog_lookup_query_from_sql(sql: str) -> str:
    match = re.search(r"'([^']+)'\s+AS\s+lookup_query", sql, flags=re.IGNORECASE)
    if not match:
        return ""
    return _extract_product_query(match.group(1))


def _row_source_scope(row: dict[str, object]) -> str:
    return _normalize_text(str(row.get("source_scope") or row.get("scope") or ""))


def _row_display_value(row: dict[str, object], *keys: str) -> str:
    for key in keys:
        value = str(row.get(key) or "").strip()
        if value:
            return value
    return ""


def _render_catalog_lookup_line(row: dict[str, object]) -> str:
    name = _row_display_value(row, "display_name", "source_name", "product_name", "name")
    if not name:
        name = "Articolo"
    supplier_name = _row_display_value(row, "display_supplier_name", "source_supplier_name", "supplier_name")
    lot_code = _row_display_value(row, "display_lot_code", "source_lot_code", "lot_code")
    suffix_parts = [part for part in (supplier_name, lot_code) if part]
    suffix = f" ({', '.join(suffix_parts)})" if suffix_parts else ""
    price = row.get("display_price_vat")
    if price is None:
        price = row.get("final_price_vat")
    price_label = _format_eur(price)
    price_suffix = f": {price_label} ivato" if price_label else ""
    return f"- {name}{suffix}{price_suffix}"


def _is_supplier_seller_lookup_message(message: str) -> bool:
    normalized = _normalize_text(message)
    return (
        "chi vende" in normalized
        or "chi mi vende" in normalized
        or (
            _contains_normalized_word(normalized, "vende", "vendono", "vendere")
            and _contains_normalized_word(normalized, "fornitore", "fornitori", "venditore", "venditori")
        )
    )


def _supplier_seller_matches(rows: list[dict[str, object]]) -> dict[str, list[str]]:
    matches: dict[str, list[str]] = {}
    for row in rows:
        supplier_name = _row_display_value(row, "display_supplier_name", "source_supplier_name", "supplier_name")
        if not supplier_name:
            continue
        product_name = _row_display_value(row, "display_name", "source_name", "product_name", "name")
        lot_code = _row_display_value(row, "display_lot_code", "source_lot_code", "lot_code")
        detail = product_name or "articolo"
        if lot_code:
            detail = f"{detail} ({lot_code})"
        supplier_matches = matches.setdefault(supplier_name, [])
        if detail not in supplier_matches:
            supplier_matches.append(detail)
    return matches


def _render_supplier_seller_lookup_result(
    query: str, supplier_rows: list[dict[str, object]], local_rows: list[dict[str, object]]
) -> str | None:
    supplier_matches = _supplier_seller_matches(supplier_rows)
    local_matches = _supplier_seller_matches(local_rows)
    if not supplier_matches and not local_matches:
        return None

    lines: list[str] = []
    if supplier_matches:
        lines.append(f"Nei cataloghi fornitori caricati {query} risulta venduto da:")
        for supplier_name in sorted(supplier_matches):
            details = "; ".join(supplier_matches[supplier_name][:5])
            lines.append(f"- {supplier_name}: {details}")

    if local_matches:
        prefix = (
            "Nel catalogo prodotti del locale risulta anche venduto da:"
            if supplier_matches
            else f"Nel catalogo prodotti del locale {query} risulta venduto da:"
        )
        lines.append(prefix)
        for supplier_name in sorted(local_matches):
            details = "; ".join(local_matches[supplier_name][:5])
            lines.append(f"- {supplier_name}: {details}")

    if local_matches and not supplier_matches:
        lines.append("Nota: nei cataloghi fornitori caricati non trovo questo articolo; il dato arriva dal catalogo prodotti del locale.")
    return "\n".join(lines)


def _row_is_semantic_match(row: dict[str, object]) -> bool:
    value = row.get("semantic_match")
    if isinstance(value, bool):
        return value
    if isinstance(value, (int, float)):
        return float(value) > 0
    return str(value or "").strip().lower() in {"1", "true", "si", "sì", "yes"}


def _render_supplier_catalog_semantic_query_result(message: str, rows: list[dict[str, object]]) -> str | None:
    semantic_rows = [
        row
        for row in rows
        if isinstance(row, dict) and str(row.get("catalog_semantic_attribute") or "").strip()
    ]
    if not semantic_rows:
        return None

    supplier_query = _extract_supplier_catalog_supplier_query(message)
    first_row = semantic_rows[0]
    lookup_supplier = _row_display_value(first_row, "lookup_supplier") or supplier_query
    subject = _row_display_value(first_row, "catalog_semantic_subject") or "prodotti"
    attribute = _row_display_value(first_row, "catalog_semantic_attribute") or "richiesti"
    matched_rows = [row for row in semantic_rows if _row_is_semantic_match(row)]
    unmatched_rows = [row for row in semantic_rows if not _row_is_semantic_match(row)]
    supplier_label = f" fornitore {lookup_supplier}" if lookup_supplier else " fornitori"

    if matched_rows:
        item_label = subject if len(matched_rows) == 1 else subject
        lines = [
            f"Nel catalogo{supplier_label} trovo {len(matched_rows)} {item_label} identificabili come {attribute}:"
        ]
        for row in matched_rows:
            reason = _row_display_value(row, "classification_reason")
            line = _render_catalog_lookup_line(row)
            if reason:
                line = f"{line} ({reason})"
            lines.append(line)
        if unmatched_rows:
            lines.append(
                f"Ho controllato anche altri {len(unmatched_rows)} articoli della categoria {subject}, "
                f"ma non hanno indicatori sufficienti nel nome/catalogo per classificarli come {attribute}."
            )
        return "\n".join(lines)

    if semantic_rows:
        lines = [
            f"Nel catalogo{supplier_label} non trovo {subject} identificabili come {attribute} dai dati salvati."
        ]
        lines.append(
            f"Ho trovato {len(semantic_rows)} articoli candidati nella categoria {subject}, ma nessuno contiene brand o indicatori affidabili per quella origine."
        )
        preview_rows = semantic_rows[:20]
        if preview_rows:
            lines.append("Candidati non classificati:")
            lines.extend(_render_catalog_lookup_line(row) for row in preview_rows)
            if len(semantic_rows) > len(preview_rows):
                lines.append(f"- ... e altri {len(semantic_rows) - len(preview_rows)} articoli")
        return "\n".join(lines)

    return None


def _render_supplier_catalog_query_result(message: str, rows: list[dict[str, object]]) -> str:
    semantic_reply = _render_supplier_catalog_semantic_query_result(message, rows)
    if semantic_reply:
        return semantic_reply

    supplier_query = _extract_supplier_catalog_supplier_query(message)
    query = _extract_supplier_catalog_product_query(message, supplier_query)
    query = query or _extract_catalog_query(message) or _extract_product_query(message) or "questa richiesta"
    supplier_rows = [
        row
        for row in rows
        if _row_source_scope(row) in {"cataloghi fornitori", "supplier catalog", "supplier catalogs"}
        or not _row_source_scope(row) and any(key in row for key in ("source_name", "source_supplier_name", "catalog_id"))
    ]
    local_rows = [
        row
        for row in rows
        if _row_source_scope(row) in {"catalogo locale", "local catalog", "catalogo prodotti"}
    ]

    if not supplier_query and _is_supplier_seller_lookup_message(message):
        seller_reply = _render_supplier_seller_lookup_result(query, supplier_rows, local_rows)
        if seller_reply:
            return seller_reply

    if not rows:
        if supplier_query:
            return f"Nel catalogo fornitore {supplier_query} caricato non trovo articoli che corrispondano a {query}."
        return (
            f"Nei cataloghi fornitori caricati non trovo articoli che corrispondano a {query}. "
            "Non trovo risultati nemmeno nel catalogo prodotti del locale."
        )

    lines: list[str] = []
    if supplier_rows:
        item_label = "articolo" if len(supplier_rows) == 1 else "articoli"
        if supplier_query:
            lines.append(f"Nel catalogo fornitore {supplier_query} trovo {len(supplier_rows)} {item_label} che corrispondono a {query}:")
        else:
            lines.append(f"Nei cataloghi fornitori caricati trovo {len(supplier_rows)} {item_label} che corrispondono a {query}:")
        lines.extend(_render_catalog_lookup_line(row) for row in supplier_rows)
    else:
        if supplier_query:
            lines.append(f"Nel catalogo fornitore {supplier_query} non trovo articoli che corrispondano a {query}.")
        else:
            lines.append(f"Nei cataloghi fornitori caricati non trovo articoli che corrispondano a {query}.")

    if local_rows and not supplier_query:
        lines.append("Nel catalogo prodotti del locale invece trovo:")
        lines.extend(_render_catalog_lookup_line(row) for row in local_rows)

    return "\n".join(lines)


def _reservation_target_label(target_date: str) -> str:
    raw_target_date = target_date.strip()
    if not raw_target_date:
        return "nel perimetro richiesto"
    try:
        target_day = date.fromisoformat(raw_target_date)
    except ValueError:
        return f"per il {raw_target_date}"
    delta_days = (target_day - _today_in_timezone()).days
    if delta_days == 0:
        return "per oggi"
    if delta_days == 1:
        return "per domani"
    if delta_days == -1:
        return "per ieri"
    return f"per il {raw_target_date}"


def _render_empty_reservations_message(
    target_date: str,
    *,
    target_time: str | None = None,
    time_window: str = "all_day",
) -> str:
    target_label = _reservation_target_label(target_date)
    if target_time:
        if target_label == "nel perimetro richiesto":
            return f"Non vedo prenotazioni alle {target_time}."
        return f"Non vedo prenotazioni {target_label} alle {target_time}."
    if time_window == "evening":
        if target_label == "per oggi":
            return "Non vedo prenotazioni per stasera."
        if target_label == "per domani":
            return "Non vedo prenotazioni per domani sera."
        if target_label == "per ieri":
            return "Non vedo prenotazioni per ieri sera."
        if target_label == "nel perimetro richiesto":
            return "Non vedo prenotazioni in fascia serale."
        return f"Non vedo prenotazioni {target_label} in fascia serale."
    if time_window == "lunch":
        if target_label == "per oggi":
            return "Non vedo prenotazioni per oggi a pranzo."
        if target_label == "per domani":
            return "Non vedo prenotazioni per domani a pranzo."
        if target_label == "per ieri":
            return "Non vedo prenotazioni per ieri a pranzo."
        if target_label == "nel perimetro richiesto":
            return "Non vedo prenotazioni in fascia pranzo."
        return f"Non vedo prenotazioni {target_label} a pranzo."
    if target_label == "nel perimetro richiesto":
        return "Non vedo prenotazioni nel perimetro richiesto."
    return f"Non vedo prenotazioni {target_label}."


def _format_inventory_units(value: object) -> str:
    try:
        numeric_value = float(value)
    except (TypeError, ValueError):
        return str(value)
    if numeric_value.is_integer():
        return str(int(numeric_value))
    return f"{numeric_value:.3f}".rstrip("0").rstrip(".").replace(".", ",")


def _inventory_row_total_units(row: dict[str, object]) -> object:
    for key in (
        "total_equivalent_units",
        "total_units",
        "sum_total_equivalent_units",
        "sum_units",
        "available_units",
        "equivalent_units",
    ):
        value = row.get(key)
        if value is not None:
            return value
    return None


def _inventory_row_is_catalog_only(row: dict[str, object]) -> bool:
    try:
        catalog_only = int(row.get("catalog_only") or 0) == 1
    except (TypeError, ValueError):
        catalog_only = False
    source = _normalize_text(str(row.get("inventory_source") or ""))
    try:
        total_units = float(_inventory_row_total_units(row) or 0)
    except (TypeError, ValueError):
        total_units = 0.0
    return catalog_only and total_units <= 0 and source in {"catalog only", "catalogo prodotti", "catalog only nessuna giacenza"}


def _format_currency(value: float) -> str:
    return f"€ {value:.2f}".replace(".", ",")


def _tips_period_label_from_message(message: str) -> str:
    explicit_date = _extract_explicit_date(message)
    if explicit_date is not None:
        return explicit_date.isoformat()

    year = _extract_reference_year(message)
    month = _extract_reference_month(message)
    if year is not None and month is not None:
        month_labels = {
            1: "gennaio",
            2: "febbraio",
            3: "marzo",
            4: "aprile",
            5: "maggio",
            6: "giugno",
            7: "luglio",
            8: "agosto",
            9: "settembre",
            10: "ottobre",
            11: "novembre",
            12: "dicembre",
        }
        return f"{month_labels.get(month, str(month))} {year}"
    if year is not None:
        return str(year)
    return ""


def _normalize_lookup(value: str) -> str:
    return _normalize_text(value).strip()


def _inventory_source_label(value: object) -> str | None:
    normalized_value = _normalize_text(str(value or ""))
    if not normalized_value:
        return None
    if "current stock" in normalized_value:
        return "contenuto corrente registrato"
    if "latest inventory" in normalized_value:
        return "ultimo inventario salvato"
    return str(value)


def _render_inventory_consumption_tool_result(result: dict[str, object]) -> str:
    reason = str(result.get("reason") or "").strip()
    start_inventory_date = str(result.get("start_inventory_date") or "").strip()
    end_inventory_date = str(result.get("end_inventory_date") or "").strip()
    items = result.get("items") if isinstance(result.get("items"), list) else []
    query = str(result.get("query") or "").strip()

    if reason == "insufficient_history" or not start_inventory_date or not end_inventory_date:
        return "Per stimare i consumi da inventario mi servono almeno due inventari totali salvati in date diverse."

    period_label = f"tra il {start_inventory_date} e il {end_inventory_date}"
    if not items:
        if query:
            return f"Non vedo cali di giacenza per {query} {period_label}."
        return f"Non vedo cali di giacenza {period_label}."

    total_consumed_units = _format_inventory_units(result.get("total_consumed_units"))
    if len(items) == 1:
        item = items[0] if isinstance(items[0], dict) else {}
        product_name = str(item.get("product_name") or "Prodotto")
        supplier_name = str(item.get("supplier_name") or "").strip()
        intro = f"Tra il {start_inventory_date} e il {end_inventory_date} risultano {total_consumed_units} unita equivalenti in meno di {product_name}"
        if supplier_name:
            intro = f"{intro} da {supplier_name}"
        intro = f"{intro}."
        lines = [intro]
        lines.append(
            f"Giacenza: {_format_inventory_units(item.get('opening_units'))} -> {_format_inventory_units(item.get('closing_units'))}."
        )
        return "\n".join(lines)

    heading = f"Tra il {start_inventory_date} e il {end_inventory_date} risultano {total_consumed_units} unita equivalenti in meno"
    if query:
        heading = f"{heading} per {query}"
    lines = [f"{heading}:"]
    for item in items:
        if not isinstance(item, dict):
            continue
        supplier_name = str(item.get("supplier_name") or "").strip()
        detail_bits = [
            f"calo {_format_inventory_units(item.get('consumed_units'))}",
            f"giacenza {_format_inventory_units(item.get('opening_units'))} -> {_format_inventory_units(item.get('closing_units'))}",
        ]
        if supplier_name:
            detail_bits.append(f"fornitore {supplier_name}")
        lines.append(f"- {item.get('product_name')}: {', '.join(detail_bits)}")
    return "\n".join(lines)


def _sum_numeric_rows(rows: list[dict[str, object]], key: str) -> float:
    total = 0.0
    for row in rows:
        try:
            total += float(row.get(key) or 0)
        except (TypeError, ValueError):
            continue
    return round(total, 3)


def _render_inventory_consumption_estimate_query_result(rows: list[dict[str, object]]) -> str:
    if not rows:
        return "Non trovo acquisti o giacenze compatibili per stimare questo consumo."

    first_row = rows[0] if isinstance(rows[0], dict) else {}
    final_inventory_date = str(first_row.get("final_inventory_date") or "").strip()
    purchase_year = str(first_row.get("purchase_year") or "").strip()
    purchase_start_date = str(first_row.get("purchase_start_date") or "").strip()
    purchase_end_date = str(first_row.get("purchase_end_date") or "").strip()
    query = str(first_row.get("query") or "").strip()
    try:
        period_days = int(first_row.get("period_days") or 365)
    except (TypeError, ValueError):
        period_days = 365

    if not final_inventory_date:
        inventory_year = str(first_row.get("inventory_year") or "").strip()
        return f"Non trovo un inventario nel {inventory_year or 'periodo richiesto'} da usare come rimanenza finale."

    normalized_rows = [row for row in rows if isinstance(row, dict)]
    total_purchased = _sum_numeric_rows(normalized_rows, "purchased_units")
    total_final = _sum_numeric_rows(normalized_rows, "final_stock_units")
    total_consumed = round(total_purchased - total_final, 3)
    total_daily = round(total_consumed / period_days, 3) if period_days > 0 else 0.0
    subject = f" per {query}" if query else ""
    scope = f" nel {purchase_year}" if purchase_year else ""

    lines = [
        f"Stima parziale consumo giornaliero{subject}{scope}: {_format_inventory_units(total_daily)} unita equivalenti al giorno.",
        (
            f"Metodo: acquisti {purchase_start_date or purchase_year} - {purchase_end_date or purchase_year} "
            f"meno giacenza del primo inventario {final_inventory_date}, stock iniziale ignorato."
        ),
        (
            f"Totale stimato: acquisti {_format_inventory_units(total_purchased)}, "
            f"rimanenza finale {_format_inventory_units(total_final)}, "
            f"consumo stimato {_format_inventory_units(total_consumed)} su {period_days} giorni."
        ),
    ]
    if total_consumed < 0:
        lines.append(
            "Attenzione: il consumo stimato risulta negativo. Significa che la rimanenza finale supera gli acquisti del periodo; senza stock iniziale il dato non e affidabile."
        )

    if len(normalized_rows) > 1:
        lines.append("Dettaglio prodotti:")
        for row in normalized_rows[:30]:
            product_name = str(row.get("product_name") or "Prodotto")
            supplier_name = str(row.get("supplier_name") or "").strip()
            supplier_fragment = f" da {supplier_name}" if supplier_name else ""
            lines.append(
                f"- {product_name}{supplier_fragment}: "
                f"{_format_inventory_units(row.get('estimated_daily_units'))}/giorno "
                f"(acquisti {_format_inventory_units(row.get('purchased_units'))}, "
                f"rimanenza {_format_inventory_units(row.get('final_stock_units'))})"
            )
        if len(normalized_rows) > 30:
            lines.append(f"...altri {len(normalized_rows) - 30} prodotti non mostrati.")

    return "\n".join(lines)


def _render_inventory_query_result(rows: list[dict[str, object]]) -> str:
    if not rows:
        return "Non vedo giacenze compatibili con questa richiesta nei magazzini registrati."

    first_row = rows[0] if rows and isinstance(rows[0], dict) else {}
    if "consumption_estimate_result" in first_row:
        return _render_inventory_consumption_estimate_query_result([row for row in rows if isinstance(row, dict)])
    if "latest_inventory_created_by_name" in first_row:
        if len(rows) == 1:
            warehouse_name = str(first_row.get("warehouse_name") or "magazzino").strip()
            created_by_name = str(first_row.get("latest_inventory_created_by_name") or "").strip() or "utente non registrato"
            latest_inventory_date = str(first_row.get("latest_inventory_date") or "").strip()
            created_at = str(first_row.get("latest_inventory_created_at") or "").replace("T", " ")[:16]
            date_fragment = f" del {latest_inventory_date}" if latest_inventory_date else ""
            saved_fragment = f", salvato il {created_at}" if created_at else ""
            return f"L'ultimo inventario{date_fragment} per {warehouse_name} risulta fatto da {created_by_name}{saved_fragment}."

        lines = ["Ultimi inventari salvati:"]
        for row in rows:
            if not isinstance(row, dict):
                continue
            warehouse_name = str(row.get("warehouse_name") or "magazzino").strip()
            created_by_name = str(row.get("latest_inventory_created_by_name") or "").strip() or "utente non registrato"
            latest_inventory_date = str(row.get("latest_inventory_date") or "").strip()
            created_at = str(row.get("latest_inventory_created_at") or "").replace("T", " ")[:16]
            saved_fragment = f", salvato il {created_at}" if created_at else ""
            lines.append(f"- {warehouse_name}: {latest_inventory_date or 'data non registrata'}, fatto da {created_by_name}{saved_fragment}")
        return "\n".join(lines)

    if "product_name" in first_row:
        if len(rows) == 1:
            product_name = str(first_row.get("product_name") or "Prodotto")
            supplier_name = str(first_row.get("supplier_name") or "").strip()
            if _inventory_row_is_catalog_only(first_row):
                intro = f"{product_name}"
                if supplier_name:
                    intro = f"{intro} da {supplier_name}"
                lot_codes = str(first_row.get("catalog_lot_codes") or "").strip()
                lot_fragment = f" Lotti catalogo: {lot_codes}." if lot_codes else ""
                return (
                    f"{intro} esiste nel catalogo prodotti, ma non risulta nessuna giacenza registrata "
                    f"negli inventari o nei magazzini attuali.{lot_fragment}"
                )
            total_units = _format_inventory_units(_inventory_row_total_units(first_row))
            intro = f"In magazzino risultano {total_units} unita equivalenti di {product_name}"
            if supplier_name:
                intro = f"{intro} da {supplier_name}"
            intro = f"{intro}."
            lines = [intro]
            warehouse_breakdown = str(first_row.get("warehouse_breakdown") or "").strip()
            if warehouse_breakdown:
                lines.append(f"Magazzini: {warehouse_breakdown}.")
            source_label = _inventory_source_label(first_row.get("inventory_source"))
            inventory_dates = str(first_row.get("inventory_dates") or "").strip()
            if inventory_dates and source_label != "contenuto corrente registrato":
                lines.append(f"Data ultimo inventario: {inventory_dates}.")
            if source_label:
                lines.append(f"Fonte: {source_label}.")
            return "\n".join(lines)

        heading = "Prodotti con maggiore giacenza totale in casa:" if "inventory_rank_result" in first_row else "Giacenze trovate nei magazzini:"
        lines = [heading]
        for row in rows:
            if not isinstance(row, dict):
                continue
            product_name = str(row.get("product_name") or "Prodotto")
            supplier_name = str(row.get("supplier_name") or "").strip()
            total_units = _format_inventory_units(_inventory_row_total_units(row))
            warehouse_breakdown = str(row.get("warehouse_breakdown") or "").strip()
            warehouse_name = str(row.get("warehouse_name") or "").strip()
            source_label = _inventory_source_label(row.get("inventory_source"))
            if _inventory_row_is_catalog_only(row):
                detail_parts = ["presente in catalogo", "nessuna giacenza registrata"]
                lot_codes = str(row.get("catalog_lot_codes") or "").strip()
                if lot_codes:
                    detail_parts.append(f"lotti catalogo {lot_codes}")
            else:
                detail_parts = [f"{total_units} unita equivalenti"]
            if supplier_name:
                detail_parts.append(f"fornitore {supplier_name}")
            if warehouse_breakdown:
                detail_parts.append(f"magazzini {warehouse_breakdown}")
            elif warehouse_name:
                detail_parts.append(f"magazzino {warehouse_name}")
            if source_label and not _inventory_row_is_catalog_only(row):
                detail_parts.append(f"fonte {source_label}")
            lines.append(f"- {product_name}: {', '.join(detail_parts)}")
        return "\n".join(lines)

    lines = ["Magazzini registrati:"]
    for row in rows:
        if not isinstance(row, dict):
            continue
        warehouse_name = str(row.get("warehouse_name") or "Magazzino")
        latest_inventory_date = str(row.get("latest_inventory_date") or "").strip()
        if latest_inventory_date:
            lines.append(
                f"- {warehouse_name}: ultimo inventario {latest_inventory_date}, "
                f"{int(row.get('latest_inventory_total_products') or 0)} prodotti, "
                f"{_format_inventory_units(row.get('latest_inventory_total_equivalent_units'))} unita equivalenti"
            )
        else:
            lines.append(
                f"- {warehouse_name}: nessun inventario datato salvato, "
                f"{int(row.get('product_count') or 0)} prodotti nel contenuto corrente, "
                f"{_format_inventory_units(row.get('current_total_equivalent_units'))} unita equivalenti"
            )
    return "\n".join(lines)


def _render_tips_query_result(message: str, rows: list[dict[str, object]]) -> str:
    if not rows:
        return "Non trovo mance che corrispondono alla richiesta."

    period_label = _tips_period_label_from_message(message)
    first_row = rows[0] if rows and isinstance(rows[0], dict) else {}
    if "total_payable_amount_sum" in first_row:
        if len(rows) == 1:
            row = first_row
            area = str(row.get("area") or "").strip()
            area_label = f" {area}" if area else ""
            tip_days = int(row.get("tip_days") or 0)
            total_tip_amount = _format_currency(float(row.get("total_tip_amount_sum") or 0))
            total_pos_amount = _format_currency(float(row.get("total_pos_amount_sum") or 0))
            total_pos_effective_amount = _format_currency(float(row.get("total_pos_effective_amount_sum") or 0))
            total_loaded_history_amount = _format_currency(float(row.get("total_loaded_history_amount_sum") or 0))
            total_payable_amount = _format_currency(float(row.get("total_payable_amount_sum") or 0))
            period_fragment = f" nel {period_label}" if period_label else ""
            lines = [f"Totale mance{area_label}{period_fragment}: {total_payable_amount} da pagare su {tip_days} giornate salvate."]
            lines.append(f"Lordo raccolto: {total_tip_amount}; POS lordo: {total_pos_amount}; POS effettivo: {total_pos_effective_amount}.")
            if float(row.get("total_loaded_history_amount_sum") or 0):
                lines.append(f"Storico caricato complessivo: {total_loaded_history_amount}.")
            return "\n".join(lines)

        lines = [f"Totali mance{f' per {period_label}' if period_label else ''}:"]
        for row in rows:
            if not isinstance(row, dict):
                continue
            area = str(row.get("area") or "area").strip()
            tip_days = int(row.get("tip_days") or 0)
            total_payable_amount = _format_currency(float(row.get("total_payable_amount_sum") or 0))
            lines.append(f"- {area}: {total_payable_amount} su {tip_days} giornate")
        return "\n".join(lines)

    if "staff_name" in first_row:
        if len(rows) == 1:
            row = first_row
            staff_name = str(row.get("staff_name") or "Dipendente")
            area = str(row.get("area") or "").strip()
            area_label = f" ({area})" if area else ""
            days_count = int(row.get("tip_days") or 0)
            assigned_total = _format_currency(float(row.get("total_assigned_amount") or 0))
            loaded_total = _format_currency(float(row.get("total_loaded_history_amount") or 0))
            visible_total = _format_currency(float(row.get("total_visible_amount") or 0))
            period_fragment = f" nel {period_label}" if period_label else ""
            lines = [f"Per {staff_name}{area_label}{period_fragment} vedo {assigned_total} di mance giornata su {days_count} giornate salvate."]
            if float(row.get("total_loaded_history_amount") or 0):
                lines.append(f"Storico caricato in calcoli successivi: {loaded_total}.")
            lines.append(f"Totale visibile nei calcoli salvati: {visible_total}.")
            return "\n".join(lines)

        lines = [f"Storico mance trovato{f' per {period_label}' if period_label else ''}:"]
        for row in rows:
            if not isinstance(row, dict):
                continue
            staff_name = str(row.get("staff_name") or "Dipendente")
            area = str(row.get("area") or "").strip()
            days_count = int(row.get("tip_days") or 0)
            visible_total = _format_currency(float(row.get("total_visible_amount") or 0))
            detail_bits = [f"{visible_total} nei calcoli", f"{days_count} giornate"]
            if area:
                detail_bits.append(area)
            lines.append(f"- {staff_name}: {', '.join(detail_bits)}")
        return "\n".join(lines)

    if "tip_date" in first_row:
        if len(rows) == 1:
            row = first_row
            tip_date = str(row.get("tip_date") or "")
            area = str(row.get("area") or "").strip()
            area_label = f" {area}" if area else ""
            total_tip_amount = _format_currency(float(row.get("total_tip_amount") or 0))
            tip_pos_amount = _format_currency(float(row.get("tip_pos_amount") or 0))
            tip_pos_effective_amount = _format_currency(float(row.get("tip_pos_effective_amount") or 0))
            total_loaded_history_amount = _format_currency(float(row.get("total_loaded_history_amount") or 0))
            total_payable_amount = _format_currency(float(row.get("total_payable_amount") or 0))
            present_staff_count = int(row.get("present_staff_count") or 0)
            lines = [f"Giornata mance{area_label} del {tip_date}: {total_payable_amount} da pagare."]
            lines.append(f"Lordo raccolto: {total_tip_amount}; POS lordo: {tip_pos_amount}; POS effettivo: {tip_pos_effective_amount}.")
            if float(row.get("total_loaded_history_amount") or 0):
                lines.append(f"Storico caricato: {total_loaded_history_amount}.")
            if present_staff_count:
                lines.append(f"Persone in divisione: {present_staff_count}.")
            return "\n".join(lines)

        lines = [f"Giornate mance salvate{f' per {period_label}' if period_label else ''}:"]
        for row in rows:
            if not isinstance(row, dict):
                continue
            tip_date = str(row.get("tip_date") or "")
            area = str(row.get("area") or "").strip()
            total_payable_amount = _format_currency(float(row.get("total_payable_amount") or 0))
            detail_bits = [total_payable_amount]
            if area:
                detail_bits.append(area)
            detail_bits.append(f"{int(row.get('present_staff_count') or 0)} persone")
            lines.append(f"- {tip_date}: {', '.join(detail_bits)}")
        return "\n".join(lines)

    lines = [f"Query mance eseguita correttamente: {len(rows)} righe restituite."]
    for row in rows:
        if not isinstance(row, dict):
            continue
        rendered_cells = [f"{key}={value}" for key, value in row.items()]
        lines.append("- " + " | ".join(rendered_cells))
    return "\n".join(lines)


def _format_homemade_quantity(value: float, measurement_unit: str) -> str:
    normalized_unit = _normalize_text(measurement_unit)
    unit = (
        "g"
        if normalized_unit in {"g", "g per liter", "g per litro", "g litro", "g l", "g per l", "g_per_liter"}
        else "drops"
        if normalized_unit in {"drops_per_liter", "drop per liter", "drops per liter", "gocce litro", "gocce per litro", "gocce/l", "gocce/litro"}
        else "ml"
    )
    if unit == "g":
        if value >= 1000:
            return f"{_format_compact_number(value / 1000)} kg"
        return f"{_format_compact_number(value)} g"
    if unit == "drops":
        return f"{_format_compact_number(value)} gocce"
    if value >= 1000:
        return f"{_format_compact_number(value / 1000)} L"
    return f"{_format_compact_number(value)} ml"


_HOMEMADE_INGREDIENT_QUERY_STOPWORDS = {
    "archivio",
    "batch",
    "c",
    "cerca",
    "cercare",
    "contenga",
    "contengono",
    "contenere",
    "contiene",
    "dentro",
    "elenco",
    "homemade",
    "ingrediente",
    "ingredienti",
    "lista",
    "prep",
    "preparazione",
    "preparazioni",
    "presente",
    "presenti",
    "ricetta",
    "ricette",
    "se",
    "trova",
    "trovare",
    "tutte",
    "tutti",
    "tutta",
    "tutto",
}


def _match_homemade_recipe_query(recipes: list[dict[str, object]], query: str) -> tuple[dict[str, object] | None, list[dict[str, object]]]:
    cleaned_query = query.strip()
    if not cleaned_query:
        return None, []
    normalized_query = _normalize_lookup(cleaned_query)
    exact_matches = [recipe for recipe in recipes if _normalize_lookup(str(recipe.get("name") or "")) == normalized_query]
    if exact_matches:
        return exact_matches[0], exact_matches
    partial_matches = [recipe for recipe in recipes if normalized_query in _normalize_lookup(str(recipe.get("name") or ""))]
    if len(partial_matches) == 1:
        return partial_matches[0], partial_matches
    return None, partial_matches


def _homemade_ingredient_search_tokens(query: str) -> list[str]:
    tokens = [
        token
        for token in _tokenize_query(query)
        if token not in _HOMEMADE_INGREDIENT_QUERY_STOPWORDS and re.search(r"[a-z0-9]", token)
    ]
    return tokens or _tokenize_query(query)


def _homemade_tokens_match(query_token: str, ingredient_token: str) -> bool:
    if _tokens_match(query_token, ingredient_token):
        return True
    if len(query_token) >= 4 and len(ingredient_token) >= 4 and query_token[:-1] == ingredient_token[:-1]:
        return True
    return False


def _homemade_ingredient_name_matches_query(ingredient_name: str, query_tokens: list[str]) -> bool:
    normalized_name = _normalize_lookup(ingredient_name)
    if not normalized_name or not query_tokens:
        return False
    ingredient_tokens = _tokenize_query(ingredient_name)
    return all(
        token in normalized_name
        or any(_homemade_tokens_match(token, ingredient_token) for ingredient_token in ingredient_tokens)
        for token in query_tokens
    )


def _format_homemade_match_quantity(value: float, measurement_unit: str) -> str:
    normalized_unit = _normalize_text(measurement_unit)
    if normalized_unit in {"part", "parte", "parti", "parts"}:
        return f"{_format_compact_number(value)} parti"
    return _format_homemade_quantity(value, measurement_unit)


def _match_homemade_recipes_by_ingredient(recipes: list[dict[str, object]], query: str) -> list[dict[str, object]]:
    query_tokens = _homemade_ingredient_search_tokens(query)
    matches: list[dict[str, object]] = []
    for recipe in recipes:
        recipe_id = str(recipe.get("id") or "")
        recipe_name = str(recipe.get("name") or "")
        matched_ingredients: list[dict[str, object]] = []
        matched_keys: set[str] = set()
        ingredients = recipe.get("ingredients") if isinstance(recipe.get("ingredients"), list) else []
        for ingredient in ingredients:
            if not isinstance(ingredient, dict):
                continue
            ingredient_name = str(ingredient.get("ingredient_name") or "").strip()
            if not _homemade_ingredient_name_matches_query(ingredient_name, query_tokens):
                continue
            measurement_unit = str(ingredient.get("measurement_unit") or "ml")
            quantity = float(ingredient.get("part_amount") or 0.0)
            matched_keys.add(_normalize_lookup(ingredient_name))
            matched_ingredients.append(
                {
                    "ingredient_name": ingredient_name,
                    "quantity": quantity,
                    "measurement_unit": measurement_unit,
                    "quantity_label": _format_homemade_match_quantity(quantity, measurement_unit),
                    "match_type": "direct",
                }
            )

        expanded_payload = _calculate_homemade_recipe_payload(recipe, recipes, target_liters=1.0)
        expanded_ingredients = (
            expanded_payload.get("expanded_ingredients")
            if isinstance(expanded_payload.get("expanded_ingredients"), list)
            else []
        )
        for ingredient in expanded_ingredients:
            if not isinstance(ingredient, dict):
                continue
            ingredient_name = str(ingredient.get("ingredient_name") or "").strip()
            ingredient_key = _normalize_lookup(ingredient_name)
            if ingredient_key in matched_keys:
                continue
            if not _homemade_ingredient_name_matches_query(ingredient_name, query_tokens):
                continue
            measurement_unit = str(ingredient.get("measurement_unit") or "ml")
            quantity = float(ingredient.get("quantity") or 0.0)
            matched_keys.add(ingredient_key)
            matched_ingredients.append(
                {
                    "ingredient_name": ingredient_name,
                    "quantity": quantity,
                    "measurement_unit": measurement_unit,
                    "quantity_label": _format_homemade_quantity(quantity, measurement_unit),
                    "match_type": "expanded",
                    "sources": ingredient.get("sources") if isinstance(ingredient.get("sources"), list) else [],
                }
            )

        if matched_ingredients:
            matches.append(
                {
                    "id": recipe_id,
                    "name": recipe_name,
                    "matched_ingredients": matched_ingredients,
                }
            )
    return sorted(matches, key=lambda item: _normalize_lookup(str(item.get("name") or "")))


def _calculate_homemade_recipe_payload(
    recipe: dict[str, object],
    recipes: list[dict[str, object]],
    *,
    target_liters: float,
) -> dict[str, object]:
    recipes_by_id = {
        str(item.get("id") or ""): item
        for item in recipes
        if str(item.get("id") or "").strip()
    }
    recipes_by_lookup = {
        _normalize_lookup(str(item.get("name") or "")): item
        for item in recipes
        if str(item.get("name") or "").strip()
    }
    expanded_ingredients: dict[str, dict[str, object]] = {}
    nested_recipes: dict[str, dict[str, object]] = {}
    warnings: list[str] = []

    def append_warning(text: str) -> None:
        if text not in warnings:
            warnings.append(text)

    def append_expanded_ingredient(ingredient_name: str, quantity: float, source_recipe_name: str, measurement_unit: str) -> None:
        normalized_measurement_unit = "ml" if _normalize_text(measurement_unit) in {"part", "parte", "parti", "parts"} else measurement_unit
        key = f"{_normalize_lookup(ingredient_name)}::{normalized_measurement_unit}"
        current = expanded_ingredients.get(key)
        if current is None:
            expanded_ingredients[key] = {
                "ingredient_name": ingredient_name,
                "quantity": quantity,
                "measurement_unit": normalized_measurement_unit,
                "sources": [source_recipe_name],
            }
            return
        current["quantity"] = float(current.get("quantity") or 0.0) + quantity
        sources = current.get("sources")
        if isinstance(sources, list) and source_recipe_name not in sources:
            sources.append(source_recipe_name)

    def expand(current_recipe: dict[str, object], current_liters: float, trail: list[str]) -> None:
        if current_liters <= 0:
            return
        recipe_id = str(current_recipe.get("id") or "")
        recipe_name = str(current_recipe.get("name") or "Prep")
        if recipe_id in trail:
            append_warning(f"Trovato un ciclo nella prep {recipe_name}. L'espansione si ferma qui.")
            return
        next_trail = [*trail, recipe_id]
        ingredients = current_recipe.get("ingredients") if isinstance(current_recipe.get("ingredients"), list) else []
        for ingredient in ingredients:
            if not isinstance(ingredient, dict):
                continue
            ingredient_name = str(ingredient.get("ingredient_name") or "").strip()
            if not ingredient_name:
                continue
            ingredient_unit = str(ingredient.get("measurement_unit") or "ml")
            per_liter_quantity = _coerce_positive_float(ingredient.get("per_liter_quantity"))
            if per_liter_quantity is None:
                continue
            scaled_quantity = per_liter_quantity * current_liters
            linked_recipe_id = str(ingredient.get("linked_recipe_id") or "").strip()
            nested_recipe = recipes_by_id.get(linked_recipe_id) if linked_recipe_id else None
            if nested_recipe is None:
                nested_recipe = recipes_by_lookup.get(_normalize_lookup(ingredient_name))
            if nested_recipe is not None and str(nested_recipe.get("id") or "") != recipe_id and ingredient_unit in {"ml", "part"}:
                nested_id = str(nested_recipe.get("id") or "")
                current_nested = nested_recipes.get(nested_id)
                if current_nested is None:
                    nested_recipes[nested_id] = {
                        "recipe_id": nested_id,
                        "recipe_name": str(nested_recipe.get("name") or ingredient_name),
                        "quantity": scaled_quantity,
                        "measurement_unit": "ml",
                    }
                else:
                    current_nested["quantity"] = float(current_nested.get("quantity") or 0.0) + scaled_quantity
                expand(nested_recipe, scaled_quantity / 1000.0, next_trail)
                continue
            append_expanded_ingredient(ingredient_name, scaled_quantity, recipe_name, ingredient_unit)

    expand(recipe, target_liters, [])

    raw_ingredients: list[dict[str, object]] = []
    for ingredient in recipe.get("ingredients") if isinstance(recipe.get("ingredients"), list) else []:
        if not isinstance(ingredient, dict):
            continue
        per_liter_quantity = _coerce_positive_float(ingredient.get("per_liter_quantity"))
        linked_recipe_id = str(ingredient.get("linked_recipe_id") or "").strip()
        raw_ingredients.append(
            {
                "ingredient_name": str(ingredient.get("ingredient_name") or ""),
                "part_amount": float(ingredient.get("part_amount") or 0.0),
                "percentage": float(ingredient.get("percentage") or 0.0),
                "quantity": (per_liter_quantity or 0.0) * target_liters,
                "measurement_unit": str(ingredient.get("measurement_unit") or "ml"),
                "linked_recipe_id": linked_recipe_id or None,
                "is_nested_recipe": bool(linked_recipe_id) or _normalize_lookup(str(ingredient.get("ingredient_name") or "")) in recipes_by_lookup,
                "calculation_mode": str(ingredient.get("calculation_mode") or "proportional"),
            }
        )

    return {
        "mode": "recipe",
        "recipe": {
            "id": str(recipe.get("id") or ""),
            "name": str(recipe.get("name") or ""),
            "notes": recipe.get("notes"),
            "total_parts": float(recipe.get("total_parts") or 0.0),
            "ingredient_count": int(recipe.get("ingredient_count") or len(raw_ingredients)),
        },
        "target_liters": target_liters,
        "raw_ingredients": raw_ingredients,
        "expanded_ingredients": sorted(
            expanded_ingredients.values(),
            key=lambda item: _normalize_lookup(str(item.get("ingredient_name") or "")),
        ),
        "nested_recipes": sorted(
            nested_recipes.values(),
            key=lambda item: _normalize_lookup(str(item.get("recipe_name") or "")),
        ),
        "warnings": warnings,
    }


def _get_homemade_recipe_tool(session: SessionIdentity, args: HomemadeRecipeArgs) -> dict[str, object]:
    payload = get_tenant_store().list_homemade_recipes(session)
    recipes = payload.get("recipes") if isinstance(payload.get("recipes"), list) else []
    normalized_recipes = [recipe for recipe in recipes if isinstance(recipe, dict)]
    query = str(args.query or "").strip()
    if not query:
        return {
            "mode": "list",
            "recipes": [
                {
                    "id": str(recipe.get("id") or ""),
                    "name": str(recipe.get("name") or ""),
                    "ingredient_count": int(recipe.get("ingredient_count") or 0),
                    "total_parts": float(recipe.get("total_parts") or 0.0),
                }
                for recipe in normalized_recipes
            ],
            "total_count": len(normalized_recipes),
        }

    matched_recipe, matches = _match_homemade_recipe_query(normalized_recipes, query)
    if matched_recipe is None:
        ingredient_matches = _match_homemade_recipes_by_ingredient(normalized_recipes, query)
        if ingredient_matches:
            return {
                "mode": "ingredient_search",
                "query": query,
                "ingredient_query": " ".join(_homemade_ingredient_search_tokens(query)) or query,
                "matches": ingredient_matches[:40],
                "total_count": len(ingredient_matches),
            }
        return {
            "mode": "ambiguous" if matches else "not_found",
            "query": query,
            "matches": [
                {
                    "id": str(recipe.get("id") or ""),
                    "name": str(recipe.get("name") or ""),
                }
                for recipe in matches[:20]
            ],
        }

    return _calculate_homemade_recipe_payload(
        matched_recipe,
        normalized_recipes,
        target_liters=args.target_liters or 1.0,
    )


def _render_homemade_tool_result(result: dict[str, object]) -> str:
    mode = str(result.get("mode") or "list")
    if mode == "list":
        recipes = result.get("recipes") if isinstance(result.get("recipes"), list) else []
        if not recipes:
            return "Non trovo ricette homemade salvate nel locale."
        lines = ["Ricette homemade del locale:"]
        for recipe in recipes:
            if not isinstance(recipe, dict):
                continue
            lines.append(
                f"- {recipe.get('name')}: {int(recipe.get('ingredient_count') or 0)} ingredienti, { _format_compact_number(recipe.get('total_parts')) } parti"
            )
        return "\n".join(lines)

    if mode == "ingredient_search":
        query = str(result.get("ingredient_query") or result.get("query") or "ingrediente richiesto")
        matches = result.get("matches") if isinstance(result.get("matches"), list) else []
        if not matches:
            return f"Non trovo ricette homemade che contengano {query}."
        lines = [f"Ricette homemade che contengono {query}:"]
        for match in matches:
            if not isinstance(match, dict):
                continue
            recipe_name = str(match.get("name") or "Ricetta")
            ingredient_rows = match.get("matched_ingredients") if isinstance(match.get("matched_ingredients"), list) else []
            details: list[str] = []
            for ingredient in ingredient_rows:
                if not isinstance(ingredient, dict):
                    continue
                ingredient_name = str(ingredient.get("ingredient_name") or "").strip()
                quantity_label = str(ingredient.get("quantity_label") or "").strip()
                match_type = str(ingredient.get("match_type") or "direct")
                suffix = "espanso da prep interne" if match_type == "expanded" else "nella ricetta base"
                if ingredient_name and quantity_label:
                    details.append(f"{ingredient_name} {quantity_label} ({suffix})")
                elif ingredient_name:
                    details.append(f"{ingredient_name} ({suffix})")
            lines.append(f"- {recipe_name}: {', '.join(details) if details else 'ingrediente trovato'}")
        return "\n".join(lines)

    if mode == "not_found":
        query = str(result.get("query") or "questa ricetta")
        return f"Non trovo una ricetta homemade che corrisponda a {query}."

    if mode == "ambiguous":
        matches = result.get("matches") if isinstance(result.get("matches"), list) else []
        if not matches:
            return "Non riesco a individuare la ricetta homemade richiesta."
        lines = ["Ho trovato piu ricette homemade compatibili. Dimmi quale vuoi usare:"]
        for match in matches:
            if isinstance(match, dict):
                lines.append(f"- {match.get('name')}")
        return "\n".join(lines)

    recipe = result.get("recipe") if isinstance(result.get("recipe"), dict) else {}
    recipe_name = str(recipe.get("name") or "Ricetta")
    target_liters = float(result.get("target_liters") or 1.0)
    expanded_ingredients = result.get("expanded_ingredients") if isinstance(result.get("expanded_ingredients"), list) else []
    nested_recipes = result.get("nested_recipes") if isinstance(result.get("nested_recipes"), list) else []
    warnings = result.get("warnings") if isinstance(result.get("warnings"), list) else []

    lines = [
        f"Ricetta homemade: {recipe_name}",
        f"Produzione richiesta: {_format_compact_number(target_liters)} L.",
    ]
    if nested_recipes:
        nested_labels = []
        for item in nested_recipes:
            if not isinstance(item, dict):
                continue
            nested_labels.append(
                f"{item.get('recipe_name')} {_format_homemade_quantity(float(item.get('quantity') or 0.0), 'ml')}"
            )
        if nested_labels:
            lines.append(f"Prep interne espanse: {', '.join(nested_labels)}.")
    if expanded_ingredients:
        lines.append("Ingredienti finali:")
        for ingredient in expanded_ingredients:
            if not isinstance(ingredient, dict):
                continue
            lines.append(
                f"- {ingredient.get('ingredient_name')}: {_format_homemade_quantity(float(ingredient.get('quantity') or 0.0), str(ingredient.get('measurement_unit') or 'ml'))}"
            )
    if warnings:
        lines.append("Note:")
        for warning in warnings:
            lines.append(f"- {warning}")
    return "\n".join(lines)


def _render_fiscal_spend_query_result(message: str, rows: list[dict[str, object]]) -> str | None:
    if not rows:
        return None
    row = rows[0]
    if not isinstance(row, dict) or "total_amount_including_vat" not in row:
        return None
    try:
        amount = float(row.get("total_amount_including_vat")) if row.get("total_amount_including_vat") is not None else None
    except (TypeError, ValueError):
        amount = None

    query = str(row.get("query") or _extract_fiscal_spend_query(message) or "").strip()
    suppliers = str(row.get("suppliers") or "").strip()
    subject = suppliers or query
    period = str(row.get("period") or "").strip()
    document_count = int(row.get("document_count") or 0)
    if amount is None or document_count <= 0:
        target = f" per {subject}" if subject else ""
        period_suffix = f" nel {period}" if re.fullmatch(r"20\d{2}", period) else f" ({period})" if period else ""
        return f"Non trovo documenti fiscali con totale IVA incluso{target}{period_suffix}."

    if re.fullmatch(r"20\d{2}", period):
        period_label = f" nel {period}"
    elif " - " in period:
        start, end = period.split(" - ", 1)
        period_label = f" dal {start} al {end}"
    elif period:
        period_label = f" ({period})"
    else:
        period_label = ""

    subject_label = f" con {subject}" if subject else ""
    basis = str(row.get("calculation_basis") or "")
    if basis == "document_total_vat_included":
        source = "totale documenti fiscali IVA inclusa"
    else:
        source = "righe documento con IVA inclusa stimata"
    return f"Abbiamo speso {_format_eur(amount)}{subject_label}{period_label} ({source})."


def _render_homemade_stock_consumption_query_result(message: str, rows: list[dict[str, object]]) -> str | None:
    if not rows:
        return "Non trovo consumi delle preparazioni homemade nel periodo richiesto."

    first_row = rows[0]
    if "homemade_consumption_result" not in first_row and "consumed_quantity" not in first_row:
        return None

    period = str(first_row.get("period_label") or "").strip() or "periodo richiesto"
    query = str(first_row.get("query") or "").strip()
    subject_suffix = f" per {query}" if query else ""
    lines = [f"Consumi preparazioni homemade{subject_suffix} ({period}):"]
    for row in rows[:50]:
        recipe_name = str(row.get("recipe_name") or row.get("name") or "Preparazione").strip()
        unit = str(row.get("measurement_unit") or "pz").strip() or "pz"
        consumed = _format_compact_number(_coerce_positive_float(row.get("consumed_quantity")) or 0)
        raw_days = _coerce_positive_float(row.get("workdays_count")) or 0
        days = _format_compact_number(raw_days)
        calculation_basis = str(row.get("calculation_basis") or "").strip()
        if calculation_basis == "operational_calendar":
            day_label = "giorno operativo calendario" if raw_days == 1 else "giorni operativi calendario"
        else:
            day_label = "giorno con consumo" if raw_days == 1 else "giorni con consumo"
        average = _format_compact_number(_coerce_positive_float(row.get("average_daily_consumption")) or 0)
        current = _format_compact_number(_coerce_positive_float(row.get("current_quantity")) or 0)
        coverage = _format_compact_number(_coerce_positive_float(row.get("coverage_days")))
        date_range = ""
        first_date = str(row.get("calculation_start_date") or row.get("first_consumed_date") or "").strip()
        last_date = str(row.get("calculation_end_date") or row.get("last_consumed_date") or "").strip()
        if first_date and last_date and first_date != last_date:
            date_range = f", dal {first_date} al {last_date}"
        elif first_date:
            date_range = f", il {first_date}"
        coverage_part = f", copertura {coverage} giorni" if coverage is not None else ""
        lines.append(
            f"- {recipe_name}: consumati {consumed} {unit} su {days} {day_label}{date_range}; "
            f"media {average} {unit}/giorno; stock attuale {current} {unit}{coverage_part}"
        )
    if len(rows) > 50:
        lines.append(f"... e altre {len(rows) - 50} preparazioni.")
    return "\n".join(lines)


def _render_single_tool_result(message: str, tool_result: dict[str, object]) -> str | None:
    tool_name = str(tool_result.get("tool") or "")
    result = tool_result.get("result")
    if not isinstance(result, dict):
        return None

    if tool_name == "describe_tenant_schema":
        tables = result.get("tables") if isinstance(result.get("tables"), list) else []
        if not tables:
            return "Non trovo tabelle disponibili nel query layer del tenant."
        lines = ["Schema dati disponibile per il tenant:"]
        for table in tables:
            if not isinstance(table, dict):
                continue
            columns = table.get("columns") if isinstance(table.get("columns"), list) else []
            column_names = [str(column.get("name") or "") for column in columns if isinstance(column, dict) and str(column.get("name") or "").strip()]
            row_count = int(table.get("row_count") or 0)
            column_label = ", ".join(column_names)
            lines.append(f"- {table.get('table')}: {row_count} righe, colonne {column_label}")
        return "\n".join(lines)

    if tool_name == "run_tenant_query":
        columns = result.get("columns") if isinstance(result.get("columns"), list) else []
        rows = result.get("rows") if isinstance(result.get("rows"), list) else []
        truncated = bool(result.get("truncated"))
        sql = str(result.get("sql") or "")
        if _sql_targets_timeclock(sql):
            return None
        if _sql_targets_tips(sql):
            return _render_tips_query_result(message, [row for row in rows if isinstance(row, dict)])
        if _sql_targets_homemade_stock(sql):
            return _render_homemade_stock_consumption_query_result(message, [row for row in rows if isinstance(row, dict)])
        if _sql_targets_inventory(sql):
            return _render_inventory_query_result([row for row in rows if isinstance(row, dict)])
        if _sql_targets_supplier_catalog(sql):
            return _render_supplier_catalog_query_result(message, [row for row in rows if isinstance(row, dict)])
        if _sql_is_fiscal_spend_query(sql):
            fiscal_reply = _render_fiscal_spend_query_result(message, [row for row in rows if isinstance(row, dict)])
            if fiscal_reply is not None:
                return fiscal_reply
        if not columns:
            return "La query e stata eseguita correttamente ma non ha restituito colonne."
        if not rows:
            normalized_message = _normalize_text(message)
            if _sql_targets_reservations(sql) or _is_reservation_subject_request(normalized_message):
                explicit_date = _extract_explicit_date(message)
                target_date = explicit_date.isoformat() if explicit_date is not None else ""
                if not target_date:
                    if "domani" in normalized_message:
                        target_date = (_today_in_timezone() + timedelta(days=1)).isoformat()
                    elif "ieri" in normalized_message:
                        target_date = (_today_in_timezone() - timedelta(days=1)).isoformat()
                    elif "oggi" in normalized_message:
                        target_date = _today_in_timezone().isoformat()
                explicit_times = _extract_explicit_times(message)
                target_time = _format_clock(explicit_times[0]) if explicit_times else None
                time_window = "evening" if "stasera" in normalized_message or "sera" in normalized_message else "lunch" if "pranzo" in normalized_message else "all_day"
                return _render_empty_reservations_message(target_date, target_time=target_time, time_window=time_window)
            return "Non trovo righe che corrispondono alla richiesta."
        lines = [f"Query SQL eseguita correttamente: {len(rows)} righe restituite" + (" (troncate)." if truncated else ".")]
        for row in rows:
            if not isinstance(row, dict):
                continue
            rendered_cells = [f"{column}={row.get(column)}" for column in columns]
            lines.append("- " + " | ".join(rendered_cells))
        return "\n".join(lines)

    if tool_name == "get_homemade_recipe":
        return _render_homemade_tool_result(result)

    if tool_name == "get_timeclock_summary":
        normalized_message = _normalize_text(message)
        scope = str(result.get("scope") or "today")
        selected_summary = result.get("selected_summary") if isinstance(result.get("selected_summary"), dict) else None
        summary_by_user = result.get("summary_by_user") if isinstance(result.get("summary_by_user"), list) else []
        active_entries = result.get("active_entries") if isinstance(result.get("active_entries"), list) else []
        entries = result.get("entries") if isinstance(result.get("entries"), list) else []
        include_entries = bool(result.get("include_entries"))

        if scope == "active":
            if not active_entries:
                return "Non vedo turni attivi in questo momento."
            lines = ["Turni attivi adesso:"]
            for entry in active_entries:
                if not isinstance(entry, dict):
                    continue
                name = entry.get("user_name") or entry.get("username") or entry.get("user_email") or "Dipendente"
                lines.append(f"- {name}: turno aperto dalle {str(entry.get('started_at') or '').replace('T', ' ')[:16]}")
            return "\n".join(lines)

        start_label = _format_italian_date_label(result.get("start_date"))
        end_label = _format_italian_date_label(result.get("end_date"))
        target_label = _format_italian_date_label(result.get("target_date"))
        if start_label and end_label and start_label == end_label:
            scope_label = f"il {start_label}"
        elif start_label and end_label:
            scope_label = f"dal {start_label} al {end_label}"
        elif target_label:
            scope_label = f"il {target_label}"
        else:
            scope_label = "oggi" if scope == "today" else "questa settimana" if scope == "week" else "nel periodo richiesto"
        first_person = any(fragment in f" {normalized_message} " for fragment in (" ho ", " io ", " mie ", " mio ", "miei", "mia"))
        if selected_summary is not None:
            total_hours = _format_duration_hours(selected_summary.get("total_hours"))
            subject_name = (
                selected_summary.get("user_name")
                or selected_summary.get("username")
                or selected_summary.get("user_email")
                or "questo account"
            )
            if first_person:
                prefix = scope_label[:1].upper() + scope_label[1:]
                lines = [f"{prefix} hai lavorato {total_hours}."]
            else:
                prefix = scope_label[:1].upper() + scope_label[1:]
                lines = [f"{prefix} {subject_name} ha lavorato {total_hours}."]
            if include_entries and entries:
                lines.append("Timbrature rilevate:")
                for entry in entries:
                    if not isinstance(entry, dict):
                        continue
                    start_label = str(entry.get("started_at") or "").replace("T", " ")[:16]
                    end_label = str(entry.get("ended_at") or "").replace("T", " ")[:16] if entry.get("ended_at") else "turno aperto"
                    duration_label = _format_duration_hours(entry.get("duration_hours"))
                    lines.append(f"- {start_label} -> {end_label} ({duration_label})")
            return "\n".join(lines)

        if summary_by_user:
            total_team_hours = sum(float(row.get("total_hours") or 0.0) for row in summary_by_user if isinstance(row, dict))
            total_team_entries = sum(int(row.get("entries") or 0) for row in summary_by_user if isinstance(row, dict))
            lines = [f"Riepilogo turni {scope_label}:"]
            lines.append(f"Totale staff: {_format_duration_hours(total_team_hours)} in {total_team_entries} turni.")
            for row in summary_by_user:
                if not isinstance(row, dict):
                    continue
                name = row.get("user_name") or row.get("username") or row.get("user_email") or "Dipendente"
                total_hours = _format_duration_hours(row.get("total_hours"))
                lines.append(f"- {name}: {total_hours} in {int(row.get('entries') or 0)} turni")
            return "\n".join(lines)

        if scope == "today":
            return f"Non vedo timbrature {scope_label}."
        if scope == "week":
            return f"Non vedo timbrature {scope_label}."
        return f"Non vedo timbrature {scope_label}."

    if tool_name == "get_inventory_consumption":
        return _render_inventory_consumption_tool_result(result)

    if tool_name == "get_reservations_snapshot":
        total = int(result.get("total_reservations") or 0)
        target_date = str(result.get("target_date") or "")
        target_time = str(result.get("target_time") or "").strip()
        time_window = str(result.get("time_window") or "all_day")
        items = result.get("items") if isinstance(result.get("items"), list) else []
        room_summaries = result.get("room_summaries") if isinstance(result.get("room_summaries"), list) else []
        slot_label = f" alle {target_time}" if target_time else ""
        normalized_message = _normalize_text(message)
        total_max_capacity = sum(
            int(room.get("max_capacity") or 0)
            for room in room_summaries
            if isinstance(room, dict)
        )
        total_available_seats = sum(
            int(room.get("available_seats") or 0)
            for room in room_summaries
            if isinstance(room, dict)
        )

        if _is_capacity_question(normalized_message) and room_summaries:
            if target_time:
                lines = [
                    f"Alle {target_time} la capienza tavoli totale del locale e di {total_max_capacity} coperti, con {total_available_seats} posti liberi."
                ]
            else:
                lines = [f"La capienza tavoli totale del locale e di {total_max_capacity} coperti."]
            for room in room_summaries:
                if not isinstance(room, dict):
                    continue
                if target_time:
                    lines.append(
                        f"- {room.get('room_name')}: {room.get('max_capacity')} coperti totali, {room.get('available_seats')} liberi, {room.get('assigned_guests')} assegnati"
                    )
                else:
                    lines.append(
                        f"- {room.get('room_name')}: {room.get('max_capacity')} coperti"
                    )
            if total == 0:
                lines.append(_render_empty_reservations_message(target_date, target_time=target_time, time_window=time_window))
            return "\n".join(lines)

        if total == 0:
            return _render_empty_reservations_message(target_date, target_time=target_time, time_window=time_window)
        label = "sera" if time_window == "evening" else "pranzo" if time_window == "lunch" else "giornata"
        if target_time:
            lines = [
                f"Per la {label} del {target_date} alle {target_time} risultano {total} prenotazioni nel perimetro richiesto, con {int(result.get('slot_guest_total') or 0)} coperti assegnati su quell'orario."
            ]
        else:
            lines = [f"Per la {label} del {target_date} risultano {total} prenotazioni per {int(result.get('total_guests') or 0)} coperti totali."]
        if room_summaries:
            lines.append("Situazione sale:")
            for room in room_summaries:
                if not isinstance(room, dict):
                    continue
                if target_time:
                    lines.append(
                        f"- {room.get('room_name')}: {room.get('assigned_guests')} coperti assegnati su {room.get('max_capacity')} massimi, {room.get('available_seats')} posti liberi, {room.get('occupied_tables')}/{room.get('table_count')} tavoli occupati"
                    )
                else:
                    lines.append(
                        f"- {room.get('room_name')}: {room.get('reservation_count')} prenotazioni, {room.get('assigned_guests')} coperti previsti, capienza tavoli {room.get('max_capacity')}, posti residui teorici {room.get('available_seats')}"
                    )
        for item in items:
            if not isinstance(item, dict):
                continue
            assignment = str(item.get("assignment") or "").strip()
            assignment_suffix = f", tavolo {assignment}" if assignment else ""
            room_name = str(item.get("room_name") or "").strip()
            room_suffix = f", sala {room_name}" if room_name else ""
            lines.append(
                f"- {item.get('start_time')}: {item.get('customer_name')} per {item.get('guests')} persone{room_suffix}{assignment_suffix}"
            )
        return "\n".join(lines)

    if tool_name in {"create_reservation", "update_reservation", "delete_reservation"}:
        status = str(result.get("status") or "")
        if status == "clarification_required":
            detail = str(result.get("detail") or "").strip()
            candidates = result.get("candidates") if isinstance(result.get("candidates"), list) else []
            if candidates:
                return f"{detail}\n{_render_reservation_candidate_lines([candidate for candidate in candidates if isinstance(candidate, dict)])}"
            return detail or "Mi servono piu dettagli per individuare la prenotazione giusta."
        if status in {"not_found", "validation_error"}:
            return str(result.get("detail") or "Operazione non completabile in modo affidabile.")
        reservation = result.get("reservation") if isinstance(result.get("reservation"), dict) else {}
        if status == "created":
            return (
                f"Ho creato la prenotazione di {reservation.get('customer_name')} per il {reservation.get('reservation_date')} "
                f"alle {reservation.get('start_time')} per {reservation.get('guests')} persone."
            )
        if status == "updated":
            return (
                f"Ho aggiornato la prenotazione di {reservation.get('customer_name')}: "
                f"{reservation.get('reservation_date')} alle {reservation.get('start_time')} per {reservation.get('guests')} persone."
            )
        if status == "deleted":
            return (
                f"Ho eliminato la prenotazione di {reservation.get('customer_name')} del "
                f"{reservation.get('reservation_date')} alle {reservation.get('start_time')}."
            )
        return None

    if tool_name == "write_shared_note":
        status = str(result.get("status") or "")
        if status == "clarification_required":
            detail = str(result.get("detail") or "").strip()
            candidates = result.get("candidates") if isinstance(result.get("candidates"), list) else []
            if candidates:
                lines = [detail]
                for candidate in candidates:
                    if isinstance(candidate, dict):
                        lines.append(f"- #{candidate.get('id')}: {candidate.get('text')}")
                return "\n".join(lines)
            return detail or "Mi serve piu contesto per capire quale nota vuoi toccare."
        if status in {"not_found", "validation_error"}:
            return str(result.get("detail") or "Operazione sulle note non completabile.")
        note = result.get("note") if isinstance(result.get("note"), dict) else {}
        if status == "created":
            return f"Ho salvato una nuova nota condivisa: {note.get('text')}"
        if status == "updated":
            return f"Ho aggiornato la nota #{note.get('id')}: {note.get('text')}"
        if status == "deleted":
            return f"Ho eliminato la nota #{note.get('id')}."
        return None

    if tool_name == "write_sales_goal":
        status = str(result.get("status") or "")
        if status == "clarification_required":
            detail = str(result.get("detail") or "").strip()
            candidates = result.get("candidates") if isinstance(result.get("candidates"), list) else []
            if candidates:
                lines = [detail]
                for candidate in candidates:
                    if isinstance(candidate, dict):
                        lines.append(f"- #{candidate.get('id')}: {candidate.get('name')} ({candidate.get('year')})")
                return "\n".join(lines)
            return detail or "Mi servono piu dettagli per gestire l'obiettivo."
        if status in {"not_found", "validation_error"}:
            return str(result.get("detail") or "Operazione sugli obiettivi non completabile.")
        goal = result.get("goal") if isinstance(result.get("goal"), dict) else {}
        target = goal.get("target")
        target_label = None
        if isinstance(target, (int, float)):
            target_label = str(int(target)) if float(target).is_integer() else str(round(float(target), 2)).replace(".", ",")
        unit_label = str(goal.get("unit_label") or ("L" if goal.get("goal_type") == "liters" else "articoli")).strip()
        scope_label = None
        if goal.get("supplier_match"):
            scope_label = f"sul fornitore {goal.get('supplier_match')}"
        elif goal.get("product_match"):
            scope_label = f"sul prodotto {goal.get('product_match')}"
        detail_bits = []
        if target_label is not None and str(goal.get("goal_type") or "") != "note":
            detail_bits.append(f"{target_label} {unit_label}".strip())
        if scope_label:
            detail_bits.append(scope_label)
        detail_suffix = f": {' '.join(detail_bits)}" if detail_bits else ""
        if status == "created":
            return f"Ho creato l'obiettivo {goal.get('year')} '{goal.get('name')}'{detail_suffix}."
        if status == "updated":
            return f"Ho aggiornato l'obiettivo {goal.get('year')} '{goal.get('name')}'{detail_suffix}."
        if status == "deleted":
            return f"Ho eliminato l'obiettivo {goal.get('year')} '{goal.get('name')}'."
        return None

    if tool_name == "write_suspended_order":
        status = str(result.get("status") or "")
        if status == "clarification_required":
            ambiguous_items = result.get("ambiguous_items") if isinstance(result.get("ambiguous_items"), list) else []
            missing_items = result.get("missing_items") if isinstance(result.get("missing_items"), list) else []
            lines = ["Non riesco ancora a chiudere l'ordine sospeso in modo affidabile."]
            for item in missing_items:
                if isinstance(item, dict):
                    lines.append(f"- Prodotto non trovato: {item.get('product_query')}")
            for item in ambiguous_items:
                if isinstance(item, dict):
                    lines.append(f"- Prodotto ambiguo: {item.get('product_query')}")
            return "\n".join(lines)
        if status == "updated":
            items = result.get("added_or_set_items") if isinstance(result.get("added_or_set_items"), list) else []
            summary = ", ".join(
                f"{item.get('quantity')} x {item.get('product_name')}"
                for item in items
                if isinstance(item, dict)
            )
            return f"Ho aggiornato l'ordine sospeso con: {summary}."
        return None

    if tool_name == "list_shared_notes":
        items = result.get("items") if isinstance(result.get("items"), list) else []
        if not items:
            return "Non risultano note condivise salvate nel locale."
        lines = ["Queste sono le ultime note condivise del locale:"]
        for item in items:
            if isinstance(item, dict):
                lines.append(f"- #{item.get('id')} {item.get('author')}: {item.get('text')}")
        return "\n".join(lines)

    if tool_name == "get_sales_goals":
        goals = result.get("goals") if isinstance(result.get("goals"), list) else []
        year = result.get("year")
        if not goals:
            return f"Per il {year} non risultano obiettivi vendita configurati."
        normalized_message = _normalize_text(message)
        if "manc" in normalized_message or "complet" in normalized_message:
            numeric_goals = [goal for goal in goals if isinstance(goal, dict) and goal.get("type") != "note"]
            note_goals = [goal for goal in goals if isinstance(goal, dict) and goal.get("type") == "note"]
            lines = [f"Per completare gli obiettivi {year} manca:"]
            incomplete_count = 0
            for goal in numeric_goals:
                if goal.get("type") == "liters_dual":
                    progress_grey = float(goal.get("progress_grey") or 0)
                    target_grey = float(goal.get("target_grey") or 0)
                    progress_patron = float(goal.get("progress_patron") or 0)
                    target_patron = float(goal.get("target_patron") or 0)
                    missing_grey = max(target_grey - progress_grey, 0)
                    missing_patron = max(target_patron - progress_patron, 0)
                    if missing_grey <= 0 and missing_patron <= 0:
                        continue
                    incomplete_count += 1
                    lines.append(
                        f"- {goal.get('name')}: Grey {_format_compact_number(missing_grey)} L mancanti "
                        f"({_format_compact_number(progress_grey)}/{_format_compact_number(target_grey)} L); "
                        f"Patron {_format_compact_number(missing_patron)} L mancanti "
                        f"({_format_compact_number(progress_patron)}/{_format_compact_number(target_patron)} L)."
                    )
                    continue
                progress = float(goal.get("progress") or 0)
                target = float(goal.get("target") or 0)
                missing = max(target - progress, 0)
                if missing <= 0:
                    continue
                incomplete_count += 1
                unit = str(goal.get("unit") or "").strip()
                lines.append(
                    f"- {goal.get('name')}: {_format_compact_number(missing)} {unit} mancanti "
                    f"({_format_compact_number(progress)}/{_format_compact_number(target)} {unit})."
                )
            if incomplete_count == 0:
                lines.append("- Tutti gli obiettivi quantitativi risultano completati.")
            if note_goals:
                lines.append("Accordi qualitativi da verificare:")
                for goal in note_goals:
                    lines.append(f"- {goal.get('name')}: {goal.get('description')}")
            return "\n".join(lines)
        if _is_sales_goal_graph_request(normalized_message):
            numeric_goals = [goal for goal in goals if isinstance(goal, dict) and goal.get("type") != "note"]
            note_goals = [goal for goal in goals if isinstance(goal, dict) and goal.get("type") == "note"]
            lines = [f"Grafico testuale obiettivi {year}:"]
            for goal in numeric_goals:
                progress = float(goal.get("progress") or 0)
                target = float(goal.get("target") or 0)
                unit = str(goal.get("unit") or "").strip()
                percentage = 0.0 if target <= 0 else max(0.0, min((progress / target) * 100.0, 100.0))
                filled = int(round((percentage / 100.0) * 10))
                bar = "█" * filled + "·" * (10 - filled)
                lines.append(
                    f"- {goal.get('name')}: [{bar}] {percentage:.1f}% ({progress}/{target} {unit})"
                )
            if note_goals:
                lines.append("Note obiettivo:")
                for goal in note_goals:
                    if isinstance(goal, dict):
                        lines.append(f"- {goal.get('name')}")
            return "\n".join(lines)
        lines = [f"Obiettivi {year} configurati nel locale:"]
        for goal in goals:
            if not isinstance(goal, dict):
                continue
            if goal.get("type") == "note":
                lines.append(f"- {goal.get('name')}: {goal.get('description')}")
            elif goal.get("type") == "liters_dual":
                lines.append(
                    f"- {goal.get('name')}: Grey {goal.get('progress_grey')}/{goal.get('target_grey')} L, Patron {goal.get('progress_patron')}/{goal.get('target_patron')} L"
                )
            else:
                lines.append(f"- {goal.get('name')}: {goal.get('progress')}/{goal.get('target')} {goal.get('unit')}")
        return "\n".join(lines)

    if tool_name == "list_tenant_users":
        users = [user for user in result.get("users", []) if isinstance(user, dict)]
        if not users:
            return "Non risultano account salvati per questo locale."

        normalized_message = _normalize_text(message)
        username_only = "username" in normalized_message and (
            any(keyword in normalized_message for keyword in ("solo", "soltanto", "solamente"))
            or "lista degli username" in normalized_message
        )
        if username_only:
            lines = ["Username degli account del locale:"]
            lines.extend(f"- {str(user.get('username') or '').strip()}" for user in users)
            return "\n".join(lines)

        lines = [f"Account del locale: {len(users)}."]
        for user in users:
            name = str(user.get("name") or user.get("username") or user.get("email") or "Account").strip()
            username = str(user.get("username") or "").strip()
            email = str(user.get("email") or "").strip()
            role = str(user.get("role") or "").strip()
            details = [part for part in (f"username {username}" if username else "", f"email {email}" if email else "", f"ruolo {role}" if role else "") if part]
            lines.append(f"- {name}: {'; '.join(details)}")
        return "\n".join(lines)

    if tool_name == "search_products":
        items = result.get("items") if isinstance(result.get("items"), list) else []
        total_count = int(result.get("count") or len(items))
        query = str(result.get("query") or "").strip()
        family_request = result.get("family_request") if isinstance(result.get("family_request"), list) else []
        family_label = ", ".join(str(item) for item in family_request if isinstance(item, str)).strip()
        request_label = family_label or query or "questa richiesta"
        if not items:
            if family_label:
                return f"Nel catalogo prodotti del locale non trovo articoli riconducibili alla famiglia {request_label}."
            return f"Nel catalogo prodotti del locale non trovo articoli che corrispondano a {request_label}."

        normalized_message = _normalize_text(message)
        catalog_query = _extract_catalog_subject_query(message) or query
        matching_items = [item for item in items if isinstance(item, dict)]
        focused_items, narrowed_focus = _focus_catalog_items(matching_items, catalog_query)
        if _is_units_per_pack_request(normalized_message):
            matching_items = focused_items or matching_items
            if len(matching_items) == 1 or (
                len(matching_items) > 1
                and float(matching_items[0].get("match_score") or 0.0) >= float(matching_items[1].get("match_score") or 0.0) + 1.2
            ):
                item = matching_items[0]
                supplier_name = str(item.get("supplier_name") or "").strip()
                lot_code = str(item.get("lot_code") or "").strip()
                suffix_parts = [part for part in (supplier_name, lot_code) if part]
                suffix = f" ({', '.join(suffix_parts)})" if suffix_parts else ""
                units_per_pack = _coerce_positive_float(item.get("units_per_pack"))
                if units_per_pack is not None:
                    return f"{item.get('product_name')}{suffix}: { _format_compact_number(units_per_pack) } unita per pack."
                if _lot_requires_units_per_pack(lot_code):
                    return f"Per {item.get('product_name')}{suffix} il dato unita per pack non e ancora salvato."
                if lot_code:
                    return f"Per {item.get('product_name')}{suffix} non serve un valore unita per pack: il lotto e {lot_code}."
                return f"Per {item.get('product_name')}{suffix} non trovo un valore unita per pack salvato."

            lines = ["Nel catalogo prodotti trovo questi formati pack:"]
            for item in matching_items:
                supplier_name = str(item.get("supplier_name") or "").strip()
                lot_code = str(item.get("lot_code") or "").strip()
                suffix_parts = [part for part in (supplier_name, lot_code) if part]
                suffix = f" ({', '.join(suffix_parts)})" if suffix_parts else ""
                units_per_pack = _coerce_positive_float(item.get("units_per_pack"))
                if units_per_pack is not None:
                    value_label = f"{_format_compact_number(units_per_pack)} unita per pack"
                elif _lot_requires_units_per_pack(lot_code):
                    value_label = "dato unita per pack mancante"
                else:
                    value_label = f"lotto {lot_code or 'non specificato'}"
                lines.append(f"- {item.get('product_name')}{suffix}: {value_label}")
            return "\n".join(lines)

        if _is_catalog_data_request(normalized_message):
            detail_items = focused_items or matching_items
            if not detail_items:
                return f"Nel catalogo prodotti del locale non trovo articoli che corrispondano a {request_label}."

            subject_label = catalog_query or str(detail_items[0].get("product_name") or "questo prodotto")
            if len({str(item.get("product_name") or "").strip() for item in detail_items}) == 1:
                subject_label = str(detail_items[0].get("product_name") or subject_label)

            lines = [f"Questi sono i dati che trovo su {subject_label}:"]
            for item in detail_items:
                product_name = str(item.get("product_name") or "").strip()
                supplier_name = str(item.get("supplier_name") or "").strip()
                lot_code = str(item.get("lot_code") or "").strip() or "non specificato"
                details = [f"lotto {lot_code}"]
                if supplier_name:
                    details.append(f"fornitore {supplier_name}")
                product_code = str(item.get("product_code") or "").strip()
                if product_code:
                    details.append(f"codice {product_code}")
                category = str(item.get("category") or "").strip()
                if category:
                    details.append(f"categoria {category}")
                price_label = _format_eur(item.get("final_price_vat"))
                if price_label:
                    details.append(f"prezzo ivato {price_label}")
                units_per_pack = _coerce_positive_float(item.get("units_per_pack"))
                if units_per_pack is not None:
                    details.append(f"unita per pack {_format_compact_number(units_per_pack)}")
                elif _lot_requires_units_per_pack(lot_code):
                    details.append("unita per pack non ancora salvata")
                liters_per_unit = _coerce_positive_float(item.get("liters_per_unit"))
                if liters_per_unit is not None:
                    details.append(f"litri per unita {_format_compact_number(liters_per_unit)}")
                total_quantity_ordered = int(item.get("total_quantity_ordered") or 0)
                if total_quantity_ordered > 0:
                    details.append(f"quantita totale ordinata {total_quantity_ordered}")
                last_ordered_at = str(item.get("last_ordered_at") or "").strip().replace("T", " ")
                if last_ordered_at:
                    details.append(f"ultimo ordine {last_ordered_at}")
                if total_quantity_ordered <= 0 and not last_ordered_at:
                    details.append("nessun ordine storico registrato")
                lines.append(f"- {product_name}: {'; '.join(details)}")
            return "\n".join(lines)

        if _is_price_per_weight_request(normalized_message):
            weighted_items: list[tuple[dict[str, object], float, float]] = []
            for item in focused_items or matching_items:
                if not isinstance(item, dict):
                    continue
                low_price, high_price = _catalog_item_price_per_kg(item)
                if low_price is None or high_price is None:
                    continue
                weighted_items.append((item, low_price, high_price))

            if not weighted_items:
                return (
                    "Trovo gli articoli richiesti nel catalogo, ma non riesco a calcolare il prezzo al chilo "
                    "perche il peso unitario non e leggibile in modo affidabile."
                )

            weighted_items.sort(key=lambda entry: ((entry[1] + entry[2]) / 2.0, str(entry[0].get("product_name") or "")))
            lines = ["Prezzo al chilo stimato nel catalogo prodotti:"]
            for item, low_price, high_price in weighted_items:
                supplier_name = str(item.get("supplier_name") or "").strip()
                lot_code = str(item.get("lot_code") or "").strip()
                suffix_parts = [part for part in (supplier_name, lot_code) if part]
                suffix = f" ({', '.join(suffix_parts)})" if suffix_parts else ""
                if abs(low_price - high_price) < 0.01:
                    price_label = f"{_format_eur(low_price)}/kg"
                else:
                    price_label = f"tra {_format_eur(low_price)}/kg e {_format_eur(high_price)}/kg"
                lines.append(f"- {item.get('product_name')}{suffix}: {price_label}")
            return "\n".join(lines)

        if _is_missing_catalog_price_request(normalized_message):
            missing_price_items = [
                item
                for item in (focused_items or matching_items)
                if isinstance(item, dict) and item.get("final_price_vat") is None
            ]
            if not missing_price_items:
                if family_label:
                    return f"Nel catalogo prodotti tutte le voci della famiglia {request_label} hanno gia un prezzo salvato."
                return f"Nel catalogo prodotti tutti gli articoli che corrispondono a {request_label} hanno gia un prezzo salvato."

            if family_label:
                heading = f"Nel catalogo prodotti non hanno ancora un prezzo salvato questi articoli della famiglia {request_label}:"
            elif narrowed_focus:
                heading = f"Nel catalogo prodotti non hanno ancora un prezzo salvato queste varianti rilevanti per {catalog_query or request_label}:"
            else:
                heading = f"Nel catalogo prodotti non hanno ancora un prezzo salvato questi articoli che corrispondono a {request_label}:"
            lines = [f"{heading} {len(missing_price_items)} articoli."]
            for item in missing_price_items:
                supplier_name = str(item.get("supplier_name") or "").strip()
                lot_code = str(item.get("lot_code") or "").strip()
                suffix_parts = [part for part in (supplier_name, lot_code) if part]
                suffix = f" ({', '.join(suffix_parts)})" if suffix_parts else ""
                lines.append(f"- {item.get('product_name')}{suffix}")
            return "\n".join(lines)

        if _is_price_request(normalized_message):
            priced_items = [
                item
                for item in (focused_items or matching_items)
                if isinstance(item, dict) and item.get("final_price_vat") is not None
            ]
            if not priced_items:
                return "Trovo gli articoli richiesti nel catalogo, ma non hanno ancora un prezzo salvato."

            if _is_lowest_price_request(normalized_message):
                cheapest_item = min(
                    priced_items,
                    key=lambda item: (
                        _coerce_positive_float(item.get("final_price_vat")) or float("inf"),
                        str(item.get("product_name") or ""),
                    ),
                )
                supplier_name = str(cheapest_item.get("supplier_name") or "").strip()
                lot_code = str(cheapest_item.get("lot_code") or "").strip()
                suffix_parts = [part for part in (supplier_name, lot_code) if part]
                suffix = f" ({', '.join(suffix_parts)})" if suffix_parts else ""
                price_label = _format_eur(cheapest_item.get("final_price_vat"))
                return f"Il meno caro in valore assoluto e {cheapest_item.get('product_name')}{suffix}: {price_label} ivato."

            if len(priced_items) == 1:
                item = priced_items[0]
                supplier_name = str(item.get("supplier_name") or "").strip()
                lot_code = str(item.get("lot_code") or "").strip()
                suffix_parts = [part for part in (supplier_name, lot_code) if part]
                suffix = f" ({', '.join(suffix_parts)})" if suffix_parts else ""
                price_label = _format_eur(item.get("final_price_vat"))
                return f"{item.get('product_name')}{suffix}: {price_label} ivato."

            top_item = priced_items[0]
            top_score = float(top_item.get("match_score") or 0.0)
            second_score = float(priced_items[1].get("match_score") or 0.0)
            if top_score >= second_score + 1.2:
                item = top_item
                supplier_name = str(item.get("supplier_name") or "").strip()
                lot_code = str(item.get("lot_code") or "").strip()
                suffix_parts = [part for part in (supplier_name, lot_code) if part]
                suffix = f" ({', '.join(suffix_parts)})" if suffix_parts else ""
                price_label = _format_eur(item.get("final_price_vat"))
                return f"{item.get('product_name')}{suffix}: {price_label} ivato."

            lines = ["Nel catalogo prodotti trovo questi prezzi ivati:"]
            for item in priced_items:
                supplier_name = str(item.get("supplier_name") or "").strip()
                lot_code = str(item.get("lot_code") or "").strip()
                suffix_parts = [part for part in (supplier_name, lot_code) if part]
                suffix = f" ({', '.join(suffix_parts)})" if suffix_parts else ""
                price_label = _format_eur(item.get("final_price_vat")) or "prezzo non disponibile"
                lines.append(f"- {item.get('product_name')}{suffix}: {price_label} ivato")
            return "\n".join(lines)

        if any(keyword in normalized_message for keyword in ("marca", "marche")):
            brands = []
            for item in focused_items or matching_items:
                if not isinstance(item, dict):
                    continue
                brand = str(item.get("likely_brand") or "").strip()
                if brand and brand not in brands:
                    brands.append(brand)
            if brands:
                return "Nel catalogo prodotti del locale risultano queste marche: " + ", ".join(brands) + "."

        display_items = focused_items or matching_items
        if family_label:
            item_label = "articolo" if total_count == 1 else "articoli"
            relation_label = "riconducibile" if total_count == 1 else "riconducibili"
            lines = [f"Nel catalogo prodotti del locale risultano {total_count} {item_label} {relation_label} alla famiglia {request_label}:"]
        elif narrowed_focus:
            item_count = len(display_items)
            item_label = "variante" if item_count == 1 else "varianti"
            lines = [f"Nel catalogo prodotti del locale trovo {item_count} {item_label} rilevanti per {catalog_query or request_label}:"]
        else:
            item_label = "articolo" if total_count == 1 else "articoli"
            verb_label = "corrisponde" if total_count == 1 else "corrispondono"
            lines = [f"Nel catalogo prodotti del locale risultano {total_count} {item_label} che {verb_label} a {request_label}:"]
        for item in display_items:
            if not isinstance(item, dict):
                continue
            lot_code = str(item.get("lot_code") or "").strip()
            supplier_name = str(item.get("supplier_name") or "").strip()
            suffix_parts = [part for part in (supplier_name, lot_code) if part]
            suffix = f" ({', '.join(suffix_parts)})" if suffix_parts else ""
            lines.append(f"- {item.get('product_name')}{suffix}")
        return "\n".join(lines)

    if tool_name == "upsert_product":
        status = str(result.get("status") or "")
        if status == "clarification_required":
            return str(result.get("detail") or "Mi servono piu dati per salvare il prodotto.")
        if status == "not_found":
            return str(result.get("detail") or "Non trovo un prodotto corrispondente.")
        product = result.get("product") if isinstance(result.get("product"), dict) else {}
        pending_fields = [
            str(field).strip()
            for field in (result.get("pending_fields") or [])
            if str(field).strip()
        ]
        suffix_parts = []
        supplier_name = str(product.get("supplier_name") or "").strip()
        lot_code = str(product.get("lot_code") or "").strip()
        if supplier_name and supplier_name != _PENDING_PRODUCT_SUPPLIER_NAME:
            suffix_parts.append(supplier_name)
        if lot_code and lot_code != _PENDING_PRODUCT_LOT_CODE:
            suffix_parts.append(lot_code)
        suffix = f" ({', '.join(suffix_parts)})" if suffix_parts else ""
        price_label = _format_eur(product.get("final_price_vat"))
        price_suffix = f" Prezzo ivato: {price_label}." if price_label else ""
        price_per_kg_label = _format_eur(product.get("unit_price_per_kg"))
        price_per_kg_suffix = f" Prezzo al kg: {price_per_kg_label}/kg." if price_per_kg_label else ""
        pending_suffix = ""
        if pending_fields:
            pending_suffix = (
                f" Restano da completare: {', '.join(pending_fields)}. "
                "Se vuoi, puoi scrivermeli qui e aggiorno subito la scheda."
            )
        if status == "created":
            return f"Ho aggiunto il prodotto {product.get('product_name')}{suffix}.{price_suffix}{price_per_kg_suffix}{pending_suffix}"
        if status == "updated":
            return f"Ho aggiornato il prodotto {product.get('product_name')}{suffix}.{price_suffix}{price_per_kg_suffix}{pending_suffix}"
        if status == "deleted":
            return f"Ho eliminato il prodotto {product.get('product_name')}{suffix}."
        return None

    if tool_name == "compare_purchase_periods":
        primary = result.get("primary") if isinstance(result.get("primary"), dict) else {}
        secondary = result.get("secondary") if isinstance(result.get("secondary"), dict) else {}
        primary_year = result.get("primary_year")
        primary_month = result.get("primary_month")
        secondary_year = result.get("secondary_year")
        secondary_month = result.get("secondary_month")
        query = str(result.get("query") or "").strip()
        normalized_message = _normalize_text(message)
        result_focus_hint = str(result.get("focus_hint") or "").strip()
        focus = result_focus_hint if result_focus_hint in {"products", "orders", "quantity", "amount"} else _purchase_comparison_focus(normalized_message, query)
        primary_label = _format_purchase_period_label(primary_year, primary_month)
        secondary_label = _format_purchase_period_label(secondary_year, secondary_month)
        lines = [f"Confronto storico ordini per {query}:" if query else "Confronto storico acquisti:"]
        primary_products = int(primary.get("distinct_products") or 0)
        secondary_products = int(secondary.get("distinct_products") or 0)
        primary_orders = int(primary.get("distinct_orders") or 0)
        secondary_orders = int(secondary.get("distinct_orders") or 0)
        primary_quantity = int(primary.get("total_quantity") or 0)
        secondary_quantity = int(secondary.get("total_quantity") or 0)
        primary_amount = _coerce_positive_float(primary.get("estimated_total_amount"))
        secondary_amount = _coerce_positive_float(secondary.get("estimated_total_amount"))
        primary_missing_prices = int(primary.get("missing_price_variant_count") or 0)
        secondary_missing_prices = int(secondary.get("missing_price_variant_count") or 0)
        if focus == "products":
            if primary_products == secondary_products:
                lines.append(
                    f"Tra {primary_label} e {secondary_label} hai ordinato lo stesso numero di prodotti distinti: {primary_products}."
                )
            elif primary_products > secondary_products:
                lines.append(
                    f"Hai ordinato piu prodotti distinti in {primary_label}: {primary_products} contro {secondary_products}."
                )
            else:
                lines.append(
                    f"Hai ordinato piu prodotti distinti in {secondary_label}: {secondary_products} contro {primary_products}."
                )
        elif focus == "orders":
            if primary_orders == secondary_orders:
                lines.append(f"Tra {primary_label} e {secondary_label} il numero di ordini distinti e uguale: {primary_orders}.")
            elif primary_orders > secondary_orders:
                lines.append(f"Hai fatto piu ordini distinti in {primary_label}: {primary_orders} contro {secondary_orders}.")
            else:
                lines.append(f"Hai fatto piu ordini distinti in {secondary_label}: {secondary_orders} contro {primary_orders}.")
        elif focus == "amount":
            if primary_amount is None and secondary_amount is None:
                return (
                    "Trovo gli ordini richiesti, ma non ho abbastanza prezzi salvati nel catalogo per stimare la spesa in modo affidabile."
                )
            primary_amount_value = primary_amount or 0.0
            secondary_amount_value = secondary_amount or 0.0
            if primary_amount_value == secondary_amount_value:
                lines.append(
                    f"Tra {primary_label} e {secondary_label} il valore stimato degli ordini e uguale: {_format_eur(primary_amount_value)}."
                )
            elif primary_amount_value > secondary_amount_value:
                lines.append(
                    f"Hai una spesa stimata maggiore in {primary_label}: {_format_eur(primary_amount_value)} contro {_format_eur(secondary_amount_value)}."
                )
            else:
                lines.append(
                    f"Hai una spesa stimata maggiore in {secondary_label}: {_format_eur(secondary_amount_value)} contro {_format_eur(primary_amount_value)}."
                )
        else:
            if primary_quantity == secondary_quantity:
                lines.append(f"Tra {primary_label} e {secondary_label} la quantita totale ordinata e uguale: {primary_quantity}.")
            elif primary_quantity > secondary_quantity:
                lines.append(f"Hai ordinato una quantita totale maggiore in {primary_label}: {primary_quantity} contro {secondary_quantity}.")
            else:
                lines.append(f"Hai ordinato una quantita totale maggiore in {secondary_label}: {secondary_quantity} contro {primary_quantity}.")
        wants_percentage = bool(result.get("percentage_requested")) or "%" in message or "percentual" in normalized_message
        if wants_percentage:
            if focus == "products":
                primary_metric = primary_products
                secondary_metric = secondary_products
                metric_label = "prodotti distinti"
            elif focus == "orders":
                primary_metric = primary_orders
                secondary_metric = secondary_orders
                metric_label = "ordini distinti"
            elif focus == "amount":
                primary_metric = primary_amount or 0.0
                secondary_metric = secondary_amount or 0.0
                metric_label = "valore stimato degli ordini"
            else:
                primary_metric = primary_quantity
                secondary_metric = secondary_quantity
                metric_label = "pezzi totali acquistati"
            if secondary_metric > 0 and primary_metric != secondary_metric:
                delta_percentage = ((primary_metric - secondary_metric) / secondary_metric) * 100
                direction = "in piu" if delta_percentage > 0 else "in meno"
                lines.append(
                    f"In {primary_label} hai ordinato il {_format_percentage(abs(delta_percentage))}% {direction} rispetto a {secondary_label}, parlando di {metric_label}."
                )
            elif secondary_metric == 0 and primary_metric > 0:
                lines.append(
                    f"In {secondary_label} il valore di riferimento sui {metric_label} e zero, quindi la percentuale non e calcolabile in modo utile."
                )
        primary_summary = f"- {primary_label}: {primary_products} prodotti distinti, {primary_orders} ordini distinti, quantita totale {primary_quantity}"
        secondary_summary = f"- {secondary_label}: {secondary_products} prodotti distinti, {secondary_orders} ordini distinti, quantita totale {secondary_quantity}"
        if primary_amount is not None:
            primary_summary += f", valore stimato {_format_eur(primary_amount)}"
        if secondary_amount is not None:
            secondary_summary += f", valore stimato {_format_eur(secondary_amount)}"
        lines.append(primary_summary)
        lines.append(secondary_summary)
        delta_products = int(result.get("delta_products") or 0)
        delta_orders = int(result.get("delta_orders") or 0)
        delta_quantity = int(result.get("delta_quantity") or 0)
        product_direction = "in piu" if delta_products > 0 else "in meno" if delta_products < 0 else "uguali"
        order_direction = "in piu" if delta_orders > 0 else "in meno" if delta_orders < 0 else "uguali"
        quantity_direction = "in piu" if delta_quantity > 0 else "in meno" if delta_quantity < 0 else "uguale"
        delta_line = (
            f"Differenza {primary_label} vs {secondary_label}: "
            f"{abs(delta_products)} prodotti {product_direction}, {abs(delta_orders)} ordini {order_direction}, {abs(delta_quantity)} pezzi {quantity_direction}."
        )
        if focus == "amount":
            primary_amount_value = primary_amount or 0.0
            secondary_amount_value = secondary_amount or 0.0
            delta_amount = primary_amount_value - secondary_amount_value
            amount_direction = "in piu" if delta_amount > 0 else "in meno" if delta_amount < 0 else "uguale"
            delta_line += f" Valore stimato {(_format_eur(abs(delta_amount)) or '€ 0,00')} {amount_direction}."
        lines.append(delta_line)
        if focus == "amount":
            if str(primary.get("pricing_basis") or "") == "order_snapshot" and str(secondary.get("pricing_basis") or "") == "order_snapshot":
                lines.append("Nota: il valore usa il prezzo snapshot salvato nelle righe ordine.")
            else:
                lines.append(
                    "Nota: il valore e stimato dove manca il prezzo snapshot storico e viene usato il prezzo ivato attuale del catalogo ordini."
                )
            if primary_missing_prices or secondary_missing_prices:
                lines.append(
                    f"Prezzi mancanti non inclusi nella stima: {primary_label} {primary_missing_prices} varianti, {secondary_label} {secondary_missing_prices} varianti."
                )
        variants = primary.get("variants") if isinstance(primary.get("variants"), list) else []
        if variants:
            lines.append(f"Varianti piu presenti in {primary_label}:")
            for variant in variants:
                if isinstance(variant, dict):
                    lines.append(
                        f"- {variant.get('product_name')} da {variant.get('supplier_name')}: {variant.get('order_count')} ordini, quantita {variant.get('total_quantity')}"
                    )
        return "\n".join(lines)

    if tool_name == "create_google_workspace_document":
        status = str(result.get("status") or "")
        if status == "clarification_required":
            return str(result.get("detail") or "Mi serve un brief piu chiaro per creare il file.")
        if status == "not_ready":
            return str(result.get("detail") or "Google Workspace non e' ancora collegato.")
        document = result.get("document") if isinstance(result.get("document"), dict) else {}
        kind = "Google Sheet" if document.get("kind") == "sheet" else "Google Doc"
        summary = str(document.get("summary") or "").strip()
        account_email = str(document.get("account_email") or "").strip()
        account_suffix = f" usando l'account {account_email}" if account_email else ""
        lines = [f"Ho creato il {kind} '{document.get('title')}'{account_suffix}."]
        if summary:
            lines.append(summary)
        web_url = str(document.get("web_url") or "").strip()
        if web_url:
            lines.append(f"Link: {web_url}")
        return "\n".join(lines)

    if tool_name == "get_purchase_history":
        items = result.get("items") if isinstance(result.get("items"), list) else []
        if not items:
            return "Non trovo righe di storico acquisti che corrispondano alla richiesta."
        total_matches = int(result.get("total_matches") or len(items))
        scope_label = _format_purchase_scope_label(
            start_date=result.get("start_date"),
            end_date=result.get("end_date"),
            year=result.get("year"),
            month=result.get("month"),
        )
        heading = "Storico acquisti rilevato"
        if scope_label:
            heading = f"{heading} {scope_label}"
        lines = [f"{heading}: {total_matches} righe trovate."]
        for item in items:
            if isinstance(item, dict):
                raw_confirmed_at = str(item.get("confirmed_at") or "")
                try:
                    confirmed_at = datetime.fromisoformat(raw_confirmed_at.replace("Z", "+00:00")).strftime("%d/%m/%Y %H:%M")
                except ValueError:
                    confirmed_at = raw_confirmed_at.replace("T", " ")
                lines.append(
                    f"- {confirmed_at}: {item.get('quantity')} x {item.get('product_name')} da {item.get('supplier_name')}"
                )
        return "\n".join(lines)

    if tool_name == "get_purchase_batches":
        batches = result.get("batches") if isinstance(result.get("batches"), list) else []
        if not batches:
            return "Non trovo ordini che corrispondano alla richiesta."
        normalized_message = _normalize_text(message)
        scope_label = _format_purchase_scope_label(
            start_date=result.get("start_date"),
            end_date=result.get("end_date"),
            year=result.get("year"),
            month=result.get("month"),
        )
        requested_rank = _extract_requested_order_rank(message)
        if requested_rank is not None and len(batches) >= requested_rank:
            batch = batches[requested_rank - 1] if isinstance(batches[requested_rank - 1], dict) else None
            if batch is None:
                return "Non trovo ordini che corrispondano alla richiesta."
            focused_quantity_reply = _render_purchase_batch_matched_quantity(message, batch, str(result.get("query") or ""))
            if focused_quantity_reply is not None:
                return focused_quantity_reply
            return "\n".join(_render_purchase_batch(batch))
        if len(batches) == 1 or (_is_latest_batches_request(normalized_message) and _extract_requested_latest_batches_limit(message) == 1):
            batch = batches[0] if isinstance(batches[0], dict) else None
            if batch is None:
                return "Non trovo ordini che corrispondano alla richiesta."
            focused_quantity_reply = _render_purchase_batch_matched_quantity(message, batch, str(result.get("query") or ""))
            if focused_quantity_reply is not None:
                return focused_quantity_reply
            return "\n".join(_render_purchase_batch(batch))

        heading = "Questi sono gli ordini rilevati"
        if scope_label:
            heading = f"{heading} {scope_label}"
        lines = [f"{heading}:"]
        for batch in batches:
            if isinstance(batch, dict):
                confirmed_at = str(batch.get("confirmed_at") or "").replace("T", " ")
                total_estimated_amount = _coerce_positive_float(batch.get("total_estimated_amount"))
                amount_suffix = f", totale {_format_eur(total_estimated_amount)}" if total_estimated_amount is not None else ""
                lines.append(
                    f"- Ordine #{batch.get('batch_id')} del {confirmed_at}: {batch.get('total_lines')} righe, quantita totale {batch.get('total_quantity')}{amount_suffix}"
                )
        return "\n".join(lines)

    if tool_name == "get_purchase_frequency":
        distinct_orders = int(result.get("distinct_orders") or 0)
        variants = result.get("variants") if isinstance(result.get("variants"), list) else []
        query = str(result.get("query") or "").strip()
        first_ordered_at = str(result.get("first_ordered_at") or "").strip().replace("T", " ")
        last_ordered_at = str(result.get("last_ordered_at") or "").strip().replace("T", " ")
        scope_label = _format_purchase_scope_label(
            start_date=result.get("start_date"),
            end_date=result.get("end_date"),
            year=result.get("year"),
            month=result.get("month"),
        )
        if distinct_orders == 0:
            if scope_label:
                return f"{scope_label[:1].upper() + scope_label[1:]} non trovo ordini che corrispondano a {query or 'questa richiesta'}."
            return f"Non trovo ordini che corrispondano a {query or 'questa richiesta'}."

        if _is_purchase_time_request(_normalize_text(message)):
            subject = query or "questa selezione"
            formatted_last_ordered_at = last_ordered_at
            if last_ordered_at:
                try:
                    last_dt = datetime.fromisoformat(last_ordered_at.replace("Z", "+00:00"))
                    formatted_last_ordered_at = last_dt.strftime("%d/%m/%Y %H:%M")
                except ValueError:
                    formatted_last_ordered_at = last_ordered_at
            lines = []
            if "da quanto" in _normalize_text(message) or "da quando" in _normalize_text(message):
                if last_ordered_at:
                    try:
                        last_dt = datetime.fromisoformat(last_ordered_at.replace("Z", "+00:00"))
                        days_delta = max((_today_in_timezone() - last_dt.date()).days, 0)
                        lines.append(f"Non ordini {subject} dal {last_dt.strftime('%d/%m/%Y %H:%M')}, cioe da {days_delta} giorni.")
                    except ValueError:
                        lines.append(f"L'ultimo ordine compatibile per {subject} e del {last_ordered_at}.")
                else:
                    lines.append(f"Non trovo una data ordine affidabile per {subject}.")
            elif formatted_last_ordered_at:
                lines.append(f"L'ultimo ordine compatibile per {subject} e del {formatted_last_ordered_at}.")
            if first_ordered_at and first_ordered_at != last_ordered_at:
                try:
                    first_dt = datetime.fromisoformat(first_ordered_at.replace("Z", "+00:00"))
                    first_label = first_dt.strftime("%d/%m/%Y %H:%M")
                except ValueError:
                    first_label = first_ordered_at
                lines.append(f"Il primo ordine rilevato nel perimetro richiesto e del {first_label}.")
            lines.append(f"Ordini distinti rilevati: {distinct_orders}. Quantita totale: {int(result.get('total_quantity') or 0)}.")
            return "\n".join(lines)

        if _is_liters_request(_normalize_text(message)):
            total_liters = result.get("total_liters")
            if total_liters is None:
                return "Trovo gli ordini richiesti, ma non ho abbastanza dati di formato per calcolare i litri in modo affidabile."

            subject = query or "questa selezione"
            lines = []
            if scope_label:
                lines.append(f"{scope_label[:1].upper() + scope_label[1:]} hai ordinato {_format_liters(total_liters)} di {subject} in {distinct_orders} ordini distinti.")
            else:
                lines.append(f"Hai ordinato {_format_liters(total_liters)} di {subject} in {distinct_orders} ordini distinti.")

            measurable_variants = [variant for variant in variants if isinstance(variant, dict) and variant.get("total_liters") is not None]
            if measurable_variants:
                lines.append("Dettaglio per variante:")
                for variant in measurable_variants:
                    lines.append(
                        f"- {variant.get('product_name')} da {variant.get('supplier_name')}: "
                        f"{_format_liters(variant.get('total_liters'))} in {variant.get('order_count')} ordini"
                    )
            missing_variants = sum(1 for variant in variants if isinstance(variant, dict) and variant.get("total_liters") is None)
            if missing_variants:
                lines.append(
                    f"Nota: {missing_variants} varianti abbinate alla richiesta non hanno un formato litro affidabile e non sono incluse nel totale."
                )
            return "\n".join(lines)

        lines = []
        subject = query or "questa selezione"
        if not query:
            if scope_label:
                lines.append(f"{scope_label[:1].upper() + scope_label[1:]} hai fatto {distinct_orders} ordini distinti.")
            else:
                lines.append(f"Hai fatto {distinct_orders} ordini distinti.")
        elif scope_label:
            lines.append(f"{scope_label[:1].upper() + scope_label[1:]} hai ordinato {subject} in {distinct_orders} ordini distinti.")
        else:
            lines.append(f"Hai ordinato {subject} in {distinct_orders} ordini distinti.")
        lines.append(f"Quantita totale rilevata: {int(result.get('total_quantity') or 0)}.")
        if not query:
            return "\n".join(lines)
        if len(variants) == 1:
            variant = variants[0]
            if isinstance(variant, dict):
                lines.append(
                    f"Variante rilevata: {variant.get('product_name')} da {variant.get('supplier_name')}, "
                    f"{variant.get('order_count')} ordini, quantita totale {variant.get('total_quantity')}."
                )
        else:
            lines.append("Dettaglio per variante:")
            for variant in variants:
                if isinstance(variant, dict):
                    lines.append(
                        f"- {variant.get('product_name')} da {variant.get('supplier_name')}: "
                        f"{variant.get('order_count')} ordini, quantita totale {variant.get('total_quantity')}"
                    )
        return "\n".join(lines)

    if tool_name == "get_purchase_overview":
        items = result.get("items") if isinstance(result.get("items"), list) else []
        normalized_message = _normalize_text(message)
        query = str(result.get("query") or "").strip()
        scope_label = _format_purchase_scope_label(
            start_date=result.get("start_date"),
            end_date=result.get("end_date"),
            year=result.get("year"),
            month=result.get("month"),
        )
        if not items:
            if _is_purchase_amount_request(normalized_message):
                estimated_total_amount = _coerce_positive_float(result.get("estimated_total_amount"))
                if estimated_total_amount is not None:
                    intro = "Ho stimato una spesa totale di"
                    if query and scope_label:
                        return f"{intro} {_format_eur(estimated_total_amount)} per {query} {scope_label}."
                    if query:
                        return f"{intro} {_format_eur(estimated_total_amount)} per {query}."
                    if scope_label:
                        return f"{intro} {_format_eur(estimated_total_amount)} {scope_label}."
                    return f"{intro} {_format_eur(estimated_total_amount)}."
                if query and scope_label:
                    return f"Per {query} {scope_label} non risultano ordini registrati, quindi la spesa totale e {_format_eur(0)}."
                if query:
                    return f"Per {query} non risultano ordini registrati, quindi la spesa totale e {_format_eur(0)}."
                if scope_label:
                    return f"{scope_label[:1].upper()}{scope_label[1:]} non risultano ordini registrati, quindi la spesa totale e {_format_eur(0)}."
                return f"Non risultano ordini registrati nel locale, quindi la spesa totale e {_format_eur(0)}."
            if not query and any(keyword in normalized_message for keyword in ("ordine", "ordini", "acquisto", "acquisti")):
                if scope_label:
                    return f"{scope_label[:1].upper()}{scope_label[1:]} non risultano ordini registrati."
                return "Non risultano ordini registrati nel locale."
            if _is_purchase_product_list_request(normalized_message) and query:
                return f"Non trovo prodotti acquistati che corrispondano a {query}."
            return "Non trovo acquisti che corrispondano alla richiesta."
        year = result.get("year")
        month = result.get("month")
        month_label = _format_italian_month(month if isinstance(month, int) else None)
        scope_parts: list[str] = []
        if month_label and year:
            scope_parts.append(f"per {month_label} {year}")
        elif month_label:
            scope_parts.append(f"per {month_label}")
        elif year:
            scope_parts.append(f"nel {year}")
        likely_brands = result.get("likely_brands") if isinstance(result.get("likely_brands"), list) else []
        if any(keyword in normalized_message for keyword in ("marca", "marche")) and likely_brands:
            brands = []
            for entry in likely_brands:
                if isinstance(entry, dict) and entry.get("brand"):
                    brands.append(str(entry.get("brand")))
            if brands:
                return "Dai prodotti acquistati risultano queste marche rilevate: " + ", ".join(brands) + "."
        suppliers = result.get("top_suppliers") if isinstance(result.get("top_suppliers"), list) else []
        matched_count = int(result.get("matched_count") or len(items))
        missing_price_variant_count = int(result.get("missing_price_variant_count") or 0)
        supplier_names = ", ".join(str(entry[0]) for entry in suppliers if isinstance(entry, list) and entry) or ", ".join(
            str(entry[0]) for entry in suppliers if isinstance(entry, tuple) and entry
        )
        if missing_price_variant_count and _is_missing_price_variants_reference_message(normalized_message):
            missing_price_items = [
                item
                for item in items
                if isinstance(item, dict) and item.get("estimated_total_amount") is None
            ]
            if missing_price_items:
                heading = "Queste sono le varianti senza prezzo disponibile"
                if query:
                    heading = f"{heading} per {query}"
                if scope_parts:
                    heading = f"{heading} {' '.join(scope_parts)}"
                lines = [f"{heading}: {len(missing_price_items)} varianti."]
                for item in missing_price_items:
                    lot_code = str(item.get("lot_code") or "").strip()
                    supplier_name = str(item.get("supplier_name") or "").strip()
                    detail_bits: list[str] = []
                    if supplier_name:
                        detail_bits.append(f"fornitore {supplier_name}")
                    if lot_code:
                        detail_bits.append(f"lotto {lot_code}")
                    if item.get("total_quantity") is not None:
                        detail_bits.append(f"quantita totale {item.get('total_quantity')}")
                    detail_suffix = f": {', '.join(detail_bits)}" if detail_bits else ""
                    lines.append(f"- {item.get('product_name')}{detail_suffix}")
                return "\n".join(lines)
        if _is_purchase_product_list_request(normalized_message) or _is_purchase_expand_followup_request(normalized_message):
            heading = "Questi sono i prodotti acquistati"
            if query:
                if len(suppliers) == 1 and isinstance(suppliers[0], (list, tuple)) and suppliers[0]:
                    heading = f"{heading} da {suppliers[0][0]}"
                else:
                    heading = f"{heading} che corrispondono a {query}"
            if scope_parts:
                heading = f"{heading} {' '.join(scope_parts)}"
            lines = [f"{heading}: {matched_count} prodotti distinti."]
            for item in items:
                if isinstance(item, dict):
                    lot_code = str(item.get("lot_code") or "").strip()
                    details = [f"quantita totale {item.get('total_quantity')}"]
                    if lot_code:
                        details.append(f"lotto {lot_code}")
                    if not (len(suppliers) == 1 and isinstance(suppliers[0], (list, tuple)) and suppliers[0]):
                        details.append(f"fornitore {item.get('supplier_name')}")
                    lines.append(f"- {item.get('product_name')}: {', '.join(details)}")
            if matched_count > len(items):
                lines.append(f"- ... e altri {matched_count - len(items)} prodotti")
            return "\n".join(lines)
        heading = "Ho trovato questi dati acquisti reali"
        if scope_parts:
            heading = f"{heading} {' '.join(scope_parts)}"
        lines = [f"{heading}:"]
        if supplier_names:
            lines.append(f"Fornitori piu presenti: {supplier_names}.")
        if _is_purchase_amount_request(normalized_message):
            estimated_total_amount = _coerce_positive_float(result.get("estimated_total_amount"))
            pricing_basis = str(result.get("pricing_basis") or "")
            if estimated_total_amount is None:
                return "Trovo gli ordini richiesti, ma non ho abbastanza prezzi salvati nel catalogo per stimare la spesa in modo affidabile."

            subject = query or "gli ordini"
            lines = []
            intro = "Ho calcolato un totale ordine di" if pricing_basis == "order_snapshot" else "Ho stimato una spesa totale di"
            if query:
                if scope_parts:
                    lines.append(f"{intro} {_format_eur(estimated_total_amount)} per {subject} {' '.join(scope_parts)}.")
                else:
                    lines.append(f"{intro} {_format_eur(estimated_total_amount)} per {subject}.")
            elif scope_parts:
                lines.append(f"{intro} {_format_eur(estimated_total_amount)} {' '.join(scope_parts)}.")
            else:
                lines.append(f"{intro} {_format_eur(estimated_total_amount)}.")
            if _is_total_only_request(normalized_message):
                return lines[0]
            if pricing_basis == "order_snapshot":
                lines.append("Nota: questo valore usa il prezzo snapshot salvato nelle righe ordine.")
            else:
                lines.append(
                    "Nota: il valore e stimato sui prezzi ivati attualmente salvati nel catalogo ordini, perche una parte dello storico non conserva il prezzo snapshot per riga."
                )
            if supplier_names:
                lines.append(f"Fornitori piu presenti: {supplier_names}.")
            priced_items = [
                item for item in items if isinstance(item, dict) and item.get("estimated_total_amount") is not None
            ]
            for item in priced_items:
                lines.append(
                    f"- {item.get('product_name')} da {item.get('supplier_name')}: {_format_eur(item.get('estimated_total_amount'))}"
                )
            if missing_price_variant_count:
                lines.append(
                    f"Prezzi mancanti non inclusi nella stima: {missing_price_variant_count} varianti."
                )
            return "\n".join(lines)
        if _is_liters_request(normalized_message):
            measurable_items = [item for item in items if isinstance(item, dict) and item.get("total_liters") is not None]
            total_liters = sum(float(item.get("total_liters") or 0.0) for item in measurable_items)
            if total_liters <= 0:
                return "Trovo acquisti che corrispondono alla richiesta, ma non ho abbastanza dati di formato per calcolare i litri in modo affidabile."

            subject = query or "questa selezione"
            lines = []
            if scope_parts:
                lines.append(f"Ho calcolato {_format_liters(total_liters)} di {subject} {' '.join(scope_parts)}.")
            else:
                lines.append(f"Ho calcolato {_format_liters(total_liters)} di {subject}.")
            if supplier_names:
                lines.append(f"Fornitori piu presenti: {supplier_names}.")
            for item in measurable_items:
                lines.append(
                    f"- {item.get('product_name')} da {item.get('supplier_name')}: {_format_liters(item.get('total_liters'))}"
                )
            missing_items = sum(1 for item in items if isinstance(item, dict) and item.get("total_liters") is None)
            if missing_items:
                lines.append(
                    f"Nota: {missing_items} articoli abbinati alla richiesta non hanno un formato litro affidabile e non sono inclusi nel totale."
                )
            return "\n".join(lines)
        if query and len(items) > 1 and _is_purchase_quantity_request(normalized_message):
            detail_items = [item for item in items if isinstance(item, dict)]
            equivalent_units = [_purchase_item_equivalent_units(item) for item in detail_items]
            lines = []
            if detail_items and all(value is not None for value in equivalent_units):
                total_equivalent_units = sum(float(value or 0) for value in equivalent_units)
                lines.append(
                    f"{' '.join(scope_parts)[:1].upper() + ' '.join(scope_parts)[1:] + ' ' if scope_parts else ''}"
                    f"abbiamo ordinato {_format_compact_number(total_equivalent_units)} unita equivalenti di {query}."
                )
            else:
                lines.append(
                    f"Trovo {len(detail_items)} varianti per {query}, ma non posso sommarle in modo affidabile perche manca il dato unita per pack su almeno un cartone."
                )
            lines.append("Dettaglio per variante:")
            for item in detail_items:
                lot_code = str(item.get("lot_code") or "").strip()
                lot_suffix = f" {lot_code}" if lot_code else ""
                lines.append(
                    f"- {item.get('product_name')} da {item.get('supplier_name')}: {item.get('total_quantity')}{lot_suffix}"
                )
            return "\n".join(lines)
        if query and len(items) == 1 and not _is_purchase_product_list_request(normalized_message):
            item = items[0]
            if isinstance(item, dict):
                product_name = str(item.get("product_name") or query).strip()
                supplier_name = str(item.get("supplier_name") or "").strip()
                lot_code = str(item.get("lot_code") or "").strip()
                quantity = int(item.get("total_quantity") or 0)
                quantity_label = f"{quantity} {lot_code}" if lot_code else str(quantity)
                scope = " ".join(scope_parts)
                prefix = f"{scope[:1].upper()}{scope[1:]} " if scope else ""
                supplier_suffix = f" da {supplier_name}" if supplier_name else ""
                return f"{prefix}abbiamo comprato {quantity_label} di {product_name}{supplier_suffix}."
        for item in items:
            if isinstance(item, dict):
                lines.append(
                    f"- {item.get('product_name')} da {item.get('supplier_name')}: quantita totale {item.get('total_quantity')}"
                )
        return "\n".join(lines)

    if tool_name == "get_suspended_order":
        items = result.get("items") if isinstance(result.get("items"), list) else []
        if not items:
            return "Al momento non c'e un ordine sospeso aperto."
        lines = ["Ordine sospeso attuale:"]
        for item in items:
            if isinstance(item, dict):
                lines.append(f"- {item.get('quantity')} x {item.get('product_name')}")
        return "\n".join(lines)

    return None


def _render_known_tool_results(message: str, tool_results: list[dict[str, object]]) -> str | None:
    if len(tool_results) == 1:
        return _render_single_tool_result(message, tool_results[0])

    last_tool_result = tool_results[-1] if tool_results else None
    if not isinstance(last_tool_result, dict):
        return None

    last_tool_name = str(last_tool_result.get("tool") or "")
    if last_tool_name == "create_google_workspace_document":
        return _render_single_tool_result(message, last_tool_result)
    if last_tool_name == "run_tenant_query":
        result = last_tool_result.get("result") if isinstance(last_tool_result.get("result"), dict) else {}
        arguments = last_tool_result.get("arguments") if isinstance(last_tool_result.get("arguments"), dict) else {}
        sql = str(result.get("sql") or arguments.get("sql") or "")
        if _sql_targets_supplier_catalog(sql) or _sql_is_fiscal_spend_query(sql) or _sql_targets_homemade_stock(sql):
            return _render_single_tool_result(message, last_tool_result)

    return None


def _final_system_prompt(session: SessionIdentity) -> str:
    return "\n".join(
        [
            "Sei l'assistente operativo globale del locale.",
            "Rispondi sempre in italiano.",
            "Rispondi come una persona competente che sta lavorando sul gestionale: frasi dirette, niente tono robotico, niente formule inutili.",
            "Usa solo i dati reali forniti nei risultati tool qui sotto.",
            "Non inventare mai numeri, clienti, prodotti, stati o azioni.",
            "Non confondere mai marca del prodotto con fornitore. Se il dato marca deriva dal nome articolo, dichiaralo chiaramente come marca rilevata dai prodotti acquistati.",
            "Se i tool non bastano o segnalano ambiguita, dillo chiaramente e chiedi il minimo dettaglio mancante.",
            "Se una richiesta e' stata eseguita, conferma in modo concreto cosa hai fatto.",
            "Se l'utente chiede dati sensibili del SUO locale autenticato, puoi rispondere usando i dati disponibili.",
            "Puoi creare liste, conteggi, percentuali e calcoli semplici solo usando i numeri forniti dall'utente o dai risultati tool.",
            "Se l'utente chiede solo un totale, restituisci solo il totale con una riga di contesto minimo. Non aggiungere liste.",
            "Se l'utente chiede una lista o una classifica, ordina e raggruppa in modo coerente con la domanda, spiegando brevemente il criterio usato.",
            "Se il risultato tool contiene SQL/righe, sintetizza dai valori restituiti senza mostrare SQL salvo richiesta esplicita.",
            "Se il risultato usa calculation_basis=document_total_vat_included, comunica che il totale viene dai documenti fiscali ed e' IVA inclusa.",
            "Non confondere il destinatario/fatturazione dentro un OCR con il fornitore normalizzato del documento fiscale.",
            "Se i risultati arrivano da inventory_latest_items, inventory_latest_lots o inventory_warehouses, trattali come giacenze reali del magazzino: usa l'ultimo inventario salvato se esiste, altrimenti il contenuto corrente registrato del magazzino.",
            "Per richieste inventario 'in casa' o senza magazzino esplicito, le soglie e i totali si intendono aggregati su tutti i magazzini. Non presentare come sotto soglia un prodotto che e' sotto soglia solo in un singolo magazzino.",
            "Per stime di consumo giornaliero o medio, spiega sempre se il calcolo e preciso o parziale. La formula affidabile richiede giacenza iniziale, acquisti del periodo, giacenza finale e numero di giorni.",
            "Se inventory_source vale current_stock, dillo chiaramente come contenuto corrente registrato e non come inventario datato. Non dedurre mai una data inventario dal nome del magazzino.",
            "Non convertire mai le unita equivalenti inventario in ml o litri se il tool non fornisce un formato certo.",
            "La data/ora corrente e il locale attivo sono forniti nel Contesto runtime del messaggio utente.",
        ]
    )


def _final_runtime_context(session: SessionIdentity) -> str:
    now = _now_in_timezone()
    context = _get_locale_profile(session)
    return "\n".join(
        [
            f"Data e ora correnti: {now.isoformat()} ({get_settings().assistant_timezone})",
            f"Locale attivo: {json.dumps(context, ensure_ascii=False)}",
        ]
    )


def _compact_for_synthesis(value: object, *, depth: int = 0) -> object:
    if value is None or isinstance(value, (bool, int, float)):
        return value
    if isinstance(value, str):
        compact = value.strip()
        if len(compact) <= _SYNTHESIS_MAX_STRING_CHARS:
            return compact
        return f"{compact[:_SYNTHESIS_MAX_STRING_CHARS]}... [testo troncato: {len(compact) - _SYNTHESIS_MAX_STRING_CHARS} caratteri]"
    if isinstance(value, list):
        max_items = _SYNTHESIS_MAX_LIST_ITEMS if depth <= 1 else 60
        compact_items = [_compact_for_synthesis(item, depth=depth + 1) for item in value[:max_items]]
        if len(value) > max_items:
            compact_items.append(
                {
                    "_truncated": True,
                    "shown_items": max_items,
                    "omitted_items": len(value) - max_items,
                }
            )
        return compact_items
    if isinstance(value, dict):
        return {str(key): _compact_for_synthesis(item, depth=depth + 1) for key, item in value.items()}
    return str(value)


def _build_tool_results_payload(tool_results: list[dict[str, object]], *, compact: bool = False) -> str:
    payload: object = _compact_for_synthesis(tool_results) if compact else tool_results
    return json.dumps(payload, ensure_ascii=False, default=str)


def _should_use_deterministic_synthesis(tool_results: list[dict[str, object]]) -> bool:
    if not tool_results:
        return False
    for tool_result in tool_results:
        if str(tool_result.get("tool") or "") != "run_tenant_query":
            continue
        result = tool_result.get("result") if isinstance(tool_result.get("result"), dict) else {}
        arguments = tool_result.get("arguments") if isinstance(tool_result.get("arguments"), dict) else {}
        sql = str(result.get("sql") or arguments.get("sql") or "")
        if _sql_is_fiscal_spend_query(sql) or _sql_targets_homemade_stock(sql) or _sql_targets_supplier_catalog(sql):
            return True
    return any(str(tool_result.get("tool") or "") in _DETERMINISTIC_SYNTHESIS_TOOLS for tool_result in tool_results)


def _snapshot_trace_value(value: object, *, depth: int = 0) -> object:
    if value is None or isinstance(value, (bool, int, float)):
        return value
    if isinstance(value, str):
        compact = " ".join(value.split())
        return compact if len(compact) <= 220 else f"{compact[:217]}..."
    if isinstance(value, list):
        if depth >= 1:
            return {"count": len(value)}
        return {
            "count": len(value),
            "sample": [_snapshot_trace_value(item, depth=depth + 1) for item in value[:3]],
        }
    if isinstance(value, dict):
        if depth >= 1:
            return {"keys": list(value.keys())[:8]}
        return {
            str(key): _snapshot_trace_value(item, depth=depth + 1)
            for key, item in list(value.items())[:8]
        }
    return str(value)


def _trace_tool_calls(tool_calls: list[PlannedToolCall]) -> list[dict[str, object]]:
    return [tool_call.model_dump() for tool_call in tool_calls]


def _trace_tool_results(tool_results: list[dict[str, object]]) -> list[dict[str, object]]:
    traced: list[dict[str, object]] = []
    for tool_result in tool_results:
        if not isinstance(tool_result, dict):
            continue
        traced.append(
            {
                "tool": tool_result.get("tool"),
                "arguments": _snapshot_trace_value(tool_result.get("arguments")),
                "result": _snapshot_trace_value(tool_result.get("result")),
            }
        )
    return traced


def _agent_tool_call_slice(tool_calls: list[PlannedToolCall]) -> list[PlannedToolCall]:
    return tool_calls[:_AGENT_MAX_TOOL_CALLS]


_READ_ONLY_AGENT_TOOLS = {
    "describe_tenant_schema",
    "run_tenant_query",
    "search_products",
    "get_purchase_overview",
    "get_purchase_history",
    "get_purchase_batches",
    "get_purchase_frequency",
    "compare_purchase_periods",
    "get_reservations_snapshot",
    "list_reservations",
    "list_fiscal_documents",
    "list_tenant_users",
    "get_timeclock_summary",
    "get_inventory_consumption",
    "get_homemade_recipe",
    "get_sales_goals",
    "list_shared_notes",
    "get_module_settings",
}


def _is_agent_read_only_plan(tool_calls: list[PlannedToolCall]) -> bool:
    return bool(tool_calls) and all(tool_call.tool in _READ_ONLY_AGENT_TOOLS for tool_call in tool_calls)


def _tool_result_empty_for_retry(tool_result: dict[str, object]) -> bool:
    tool_name = str(tool_result.get("tool") or "")
    result = tool_result.get("result")
    if tool_name == "describe_tenant_schema":
        return False
    if not isinstance(result, dict):
        return False

    if tool_name == "run_tenant_query":
        return _run_tenant_query_result_looks_empty(result)
    if tool_name == "search_products":
        return int(result.get("count") or 0) == 0
    if tool_name in {"get_purchase_overview", "get_purchase_frequency"}:
        return int(result.get("matched_count") or result.get("count") or 0) == 0
    if tool_name == "get_purchase_history":
        return int(result.get("total_matches") or result.get("count") or 0) == 0
    if tool_name == "get_purchase_batches":
        batches = result.get("batches") if isinstance(result.get("batches"), list) else []
        return not batches
    if tool_name == "compare_purchase_periods":
        primary = result.get("primary") if isinstance(result.get("primary"), dict) else {}
        secondary = result.get("secondary") if isinstance(result.get("secondary"), dict) else {}
        primary_matches = max(
            int(primary.get("matched_rows") or 0),
            int(primary.get("distinct_orders") or 0),
            int(primary.get("distinct_products") or 0),
        )
        secondary_matches = max(
            int(secondary.get("matched_rows") or 0),
            int(secondary.get("distinct_orders") or 0),
            int(secondary.get("distinct_products") or 0),
        )
        return primary_matches == 0 and secondary_matches == 0
    if tool_name in {"list_fiscal_documents", "list_tenant_users", "list_shared_notes"}:
        return int(result.get("count") or 0) == 0
    if tool_name == "get_inventory_consumption":
        items = result.get("items") if isinstance(result.get("items"), list) else []
        return not items
    if tool_name == "get_homemade_recipe":
        return str(result.get("mode") or "") in {"not_found", "ambiguous"}
    if tool_name == "get_sales_goals":
        goals = result.get("goals") if isinstance(result.get("goals"), list) else []
        return not goals
    if tool_name == "get_module_settings":
        settings = result.get("settings")
        return isinstance(settings, dict) and not settings
    return False


def _is_empty_aggregate_cell(value: object) -> bool:
    if value is None:
        return True
    if isinstance(value, (int, float)):
        return float(value) == 0.0
    if isinstance(value, str):
        stripped = value.strip()
        if not stripped:
            return True
        try:
            return float(stripped.replace(",", ".")) == 0.0
        except ValueError:
            return False
    return False


def _run_tenant_query_result_looks_empty(result: dict[str, object]) -> bool:
    if int(result.get("row_count") or 0) == 0:
        return True
    rows = result.get("rows") if isinstance(result.get("rows"), list) else []
    if len(rows) != 1 or not isinstance(rows[0], dict):
        return False
    row = rows[0]
    aggregate_keys = {
        key
        for key in row
        if any(fragment in str(key).lower() for fragment in ("total", "sum", "count", "quantity", "amount", "units", "ore", "hours"))
    }
    if not aggregate_keys:
        return False
    return all(_is_empty_aggregate_cell(row.get(key)) for key in aggregate_keys)


def _tool_results_need_agent_retry(tool_calls: list[PlannedToolCall], tool_results: list[dict[str, object]]) -> bool:
    if not _is_agent_read_only_plan(tool_calls):
        return False
    actionable_results = [
        tool_result
        for tool_result in tool_results
        if str(tool_result.get("tool") or "") != "describe_tenant_schema"
    ]
    if not actionable_results and any(str(tool_result.get("tool") or "") == "describe_tenant_schema" for tool_result in tool_results):
        return True
    return bool(actionable_results) and any(_tool_result_empty_for_retry(tool_result) for tool_result in actionable_results)


def _tool_calls_signature(tool_calls: list[PlannedToolCall]) -> str:
    return json.dumps([tool_call.model_dump() for tool_call in tool_calls], sort_keys=True, default=str)


async def _replan_after_tool_results(
    *,
    session: SessionIdentity,
    message: str,
    conversation: list[dict[str, str]],
    thread_state: dict[str, object] | None,
    previous_tool_calls: list[PlannedToolCall],
    previous_tool_results: list[dict[str, object]],
) -> AssistantPlan | None:
    retry_messages = [
        {"role": "system", "content": _planner_system_prompt(session)},
        {
            "role": "user",
            "content": "\n".join(
                [
                    "Contesto runtime:",
                    _planner_runtime_context(session),
                    "",
                    "Stato strutturato del thread:",
                    _build_home_thread_state_summary(thread_state),
                    "",
                    "Conversazione recente:",
                    _build_recent_conversation(conversation),
                    "",
                    f"Messaggio utente attuale: {message.strip()}",
                    "",
                    "Primo piano eseguito:",
                    json.dumps([tool_call.model_dump() for tool_call in previous_tool_calls], ensure_ascii=False, default=str),
                    "",
                    "Risultati reali del primo piano:",
                    _build_tool_results_payload(previous_tool_results, compact=True),
                    "",
                    "Se i risultati sono vuoti, incompleti o sembrano aver interrogato la fonte sbagliata, costruisci un nuovo piano usando tool diversi o una query SQL migliore. "
                    "Non ripetere lo stesso piano identico. Se invece i risultati sono corretti e indicano davvero assenza di dati, usa mode=reply e spiegalo chiaramente.",
                ]
            ),
        },
    ]
    reply, _ = await request_llm_chat_completion(
        retry_messages,
        temperature=0.0,
        max_tokens=get_settings().assistant_max_tokens,
        model=_assistant_planner_model(),
    )
    try:
        parsed = _normalize_plan_payload(_parse_json_object(reply))
        retry_plan = AssistantPlan.model_validate(parsed)
    except (ValueError, ValidationError):
        return None
    if retry_plan.mode == "tool" and _tool_calls_signature(retry_plan.tool_calls) == _tool_calls_signature(previous_tool_calls):
        return None
    return retry_plan


async def _force_tool_plan_for_grounded_request(
    *,
    session: SessionIdentity,
    message: str,
    conversation: list[dict[str, str]],
    thread_state: dict[str, object] | None,
    rejected_reply: str,
) -> AssistantPlan | None:
    force_messages = [
        {"role": "system", "content": _planner_system_prompt(session)},
        {
            "role": "user",
            "content": "\n".join(
                [
                    "Contesto runtime:",
                    _planner_runtime_context(session),
                    "",
                    "Stato strutturato del thread:",
                    _build_home_thread_state_summary(thread_state),
                    "",
                    "Conversazione recente:",
                    _build_recent_conversation(conversation),
                    "",
                    f"Messaggio utente attuale: {message.strip()}",
                    "",
                    "Il planner aveva risposto senza tool, ma questa e' una richiesta grounded sui dati reali del locale.",
                    f"Risposta rifiutata: {rejected_reply.strip() or '(vuota)'}",
                    "",
                    "Devi ora restituire mode=tool con i tool necessari. Non usare placeholder, non inventare dati e non usare mode=reply.",
                ]
            ),
        },
    ]
    reply, _ = await request_llm_chat_completion(
        force_messages,
        temperature=0.0,
        max_tokens=get_settings().assistant_max_tokens,
        model=_assistant_planner_model(),
    )
    try:
        parsed = _normalize_plan_payload(_parse_json_object(reply))
        forced_plan = AssistantPlan.model_validate(parsed)
    except (ValueError, ValidationError):
        return None
    return forced_plan if forced_plan.mode == "tool" and forced_plan.tool_calls else None


def _should_fallback_to_direct_after_planner_results(
    message: str,
    normalized: str,
    planned_tool_results: list[dict[str, object]],
    direct_tool_calls: list[PlannedToolCall],
) -> bool:
    if not direct_tool_calls:
        return False
    direct_read_only = _is_agent_read_only_plan(_agent_tool_call_slice(direct_tool_calls))
    planner_results_look_empty = any(
        _tool_result_empty_for_retry(tool_result)
        for tool_result in planned_tool_results
        if isinstance(tool_result, dict)
    )
    if direct_read_only and planner_results_look_empty:
        return True
    has_direct_inventory_query = any(
        tool_call.tool == "run_tenant_query"
        and _sql_targets_inventory(str(tool_call.arguments.get("sql") or ""))
        for tool_call in direct_tool_calls
    )
    if _is_inventory_request(message, normalized) and has_direct_inventory_query:
        for tool_result in planned_tool_results:
            if not isinstance(tool_result, dict) or tool_result.get("tool") != "run_tenant_query":
                continue
            result = tool_result.get("result") if isinstance(tool_result.get("result"), dict) else {}
            arguments = tool_result.get("arguments") if isinstance(tool_result.get("arguments"), dict) else {}
            sql = str(result.get("sql") or arguments.get("sql") or "")
            if _sql_targets_inventory(sql) and _run_tenant_query_result_looks_empty(result):
                return True
    has_direct_catalog_lookup = any(
        tool_call.tool == "run_tenant_query"
        and _sql_targets_supplier_catalog(str(tool_call.arguments.get("sql") or ""))
        for tool_call in direct_tool_calls
    )
    if not any(tool_call.tool == "search_products" for tool_call in direct_tool_calls) and not has_direct_catalog_lookup:
        return False
    catalog_query = _extract_catalog_query(message)
    if not catalog_query or not (_is_catalog_request(message, normalized, catalog_query) or _is_supplier_catalog_request(normalized)):
        return False

    for tool_result in planned_tool_results:
        if not isinstance(tool_result, dict):
            continue
        if tool_result.get("tool") != "run_tenant_query":
            continue
        result = tool_result.get("result") if isinstance(tool_result.get("result"), dict) else {}
        if _run_tenant_query_result_looks_empty(result):
            return True
    return False


async def _synthesize_from_tool_results(
    *,
    session: SessionIdentity,
    message: str,
    conversation: list[dict[str, str]],
    tool_results: list[dict[str, object]],
) -> tuple[str, str]:
    deterministic_reply = _render_known_tool_results(message, tool_results)
    if deterministic_reply and _should_use_deterministic_synthesis(tool_results):
        return deterministic_reply, "operational-deterministic"

    synthesis_messages = [
        {"role": "system", "content": _final_system_prompt(session)},
        {
            "role": "user",
            "content": "\n".join(
                [
                    "Contesto runtime:",
                    _final_runtime_context(session),
                    "",
                    "Conversazione recente:",
                    _build_recent_conversation(conversation),
                    "",
                    f"Messaggio utente attuale: {message}",
                    "",
                    "Risultati tool reali:",
                    _build_tool_results_payload(tool_results, compact=True),
                ]
            ),
        },
    ]

    try:
        reply, model = await request_llm_chat_completion(
            synthesis_messages,
            temperature=0.1,
            model=_assistant_synthesis_model(),
        )
        return reply.strip(), model
    except HTTPException:
        if deterministic_reply:
            return deterministic_reply, "operational-deterministic-fallback"
        raise


def _timeclock_access_denied_run(*, normalized_thread_state: dict[str, object], message: str) -> OperationalAssistantRun:
    return OperationalAssistantRun(
        reply="Non possiedi l'autorizzazione per usare Turni. Chiedi all'amministratore del locale di abilitare questo accesso dal pannello Account.",
        model="policy",
        route="timeclock-access-denied",
        trace={
            "surface": "home",
            "message": message,
            "reason": "timeclock_permission_missing",
            "thread_state_before": _snapshot_trace_value(normalized_thread_state),
        },
        thread_state=normalized_thread_state,
    )


def _inventory_access_denied_run(*, normalized_thread_state: dict[str, object], message: str) -> OperationalAssistantRun:
    return OperationalAssistantRun(
        reply="Non possiedi l'autorizzazione per usare Inventario. Chiedi all'amministratore del locale di abilitare questo accesso dal pannello Account.",
        model="policy",
        route="inventory-access-denied",
        trace={
            "surface": "home",
            "message": message,
            "reason": "inventory_permission_missing",
            "thread_state_before": _snapshot_trace_value(normalized_thread_state),
        },
        thread_state=normalized_thread_state,
    )


def _homemade_access_denied_run(*, normalized_thread_state: dict[str, object], message: str) -> OperationalAssistantRun:
    return OperationalAssistantRun(
        reply="Non possiedi l'autorizzazione per usare Homemade. Chiedi all'amministratore del locale di abilitare questo accesso dal pannello Account.",
        model="policy",
        route="homemade-access-denied",
        trace={
            "surface": "home",
            "message": message,
            "reason": "homemade_permission_missing",
            "thread_state_before": _snapshot_trace_value(normalized_thread_state),
        },
        thread_state=normalized_thread_state,
    )


async def run_operational_assistant_with_trace(
    *,
    session: SessionIdentity,
    message: str,
    conversation: list[dict[str, str]],
    thread_state: dict[str, object] | None = None,
) -> OperationalAssistantRun:
    trimmed_message = message.strip()
    normalized_thread_state = _normalize_home_thread_state(thread_state)
    normalized_message = _normalize_text(trimmed_message)
    # Do not deny access from keyword parsing alone: the planner chooses the real tool,
    # then tool-level scope checks enforce permissions deterministically.
    heuristic_missing_scopes_for_message = _assistant_missing_scopes_for_message(session, trimmed_message, normalized_message)
    if _DIRECT_GREETING_PATTERN.match(trimmed_message):
        venue_name = _get_locale_profile(session)["venue_name"]
        reply = (
            f"Ciao. Sono qui per aiutarti sulla gestione di {venue_name}: prenotazioni, prodotti, ordini sospesi, note e storico acquisti. Dimmi pure cosa ti serve."
        )
        return OperationalAssistantRun(
            reply=reply,
            model="operational-direct",
            route="direct-greeting",
            trace={"surface": "home", "message": trimmed_message, "thread_state_before": _snapshot_trace_value(normalized_thread_state)},
            thread_state=normalized_thread_state,
        )

    pack_size_followup_clarification = _build_pack_size_followup_clarification(trimmed_message, conversation, normalized_thread_state)
    if pack_size_followup_clarification:
        next_thread_state = _derive_home_thread_state(
            previous_state=normalized_thread_state,
            message=trimmed_message,
            executed_tool_calls=[],
            tool_results=[],
            route="deterministic-pack-size-clarification",
            conversation=conversation,
        )
        return OperationalAssistantRun(
            reply=pack_size_followup_clarification,
            model="operational-deterministic",
            route="deterministic-pack-size-clarification",
            trace={
                "surface": "home",
                "message": trimmed_message,
                "normalized_message": normalized_message,
                "thread_state_before": _snapshot_trace_value(normalized_thread_state),
                "thread_state_after": _snapshot_trace_value(next_thread_state),
            },
            thread_state=next_thread_state,
        )
    product_write_clarification = _build_product_write_clarification(trimmed_message, conversation, normalized_thread_state)
    if product_write_clarification:
        if product_write_clarification.startswith("Non risulta nessun nuovo prodotto appena creato"):
            next_thread_state = normalized_thread_state
        else:
            next_thread_state = _derive_home_thread_state(
                previous_state=normalized_thread_state,
                message=trimmed_message,
                executed_tool_calls=[],
                tool_results=[],
                route="deterministic-product-clarification",
                conversation=conversation,
            )
        return OperationalAssistantRun(
            reply=product_write_clarification,
            model="operational-deterministic",
            route="deterministic-product-clarification",
            trace={
                "surface": "home",
                "message": trimmed_message,
                "normalized_message": normalized_message,
                "thread_state_before": _snapshot_trace_value(normalized_thread_state),
                "thread_state_after": _snapshot_trace_value(next_thread_state),
            },
            thread_state=next_thread_state,
        )
    sales_goal_write_clarification = _build_sales_goal_write_clarification(session, trimmed_message, conversation, normalized_thread_state)
    if sales_goal_write_clarification:
        next_thread_state = _derive_home_thread_state(
            previous_state=normalized_thread_state,
            message=trimmed_message,
            executed_tool_calls=[],
            tool_results=[],
            route="deterministic-sales-goal-clarification",
            conversation=conversation,
        )
        return OperationalAssistantRun(
            reply=sales_goal_write_clarification,
            model="operational-deterministic",
            route="deterministic-sales-goal-clarification",
            trace={
                "surface": "home",
                "message": trimmed_message,
                "normalized_message": normalized_message,
                "thread_state_before": _snapshot_trace_value(normalized_thread_state),
                "thread_state_after": _snapshot_trace_value(next_thread_state),
            },
            thread_state=next_thread_state,
        )
    reservation_create_clarification = _build_reservation_create_clarification(trimmed_message)
    if reservation_create_clarification:
        return OperationalAssistantRun(
            reply=reservation_create_clarification,
            model="operational-deterministic",
            route="deterministic-reservation-clarification",
            trace={"surface": "home", "message": trimmed_message, "normalized_message": normalized_message, "thread_state_before": _snapshot_trace_value(normalized_thread_state)},
            thread_state=normalized_thread_state,
        )
    grounded_request = _is_grounded_data_request(trimmed_message, normalized_message)
    contextual_tool_calls = _filter_tool_calls_for_surface("home", _build_contextual_tool_calls(trimmed_message, conversation, normalized_thread_state))
    direct_tool_calls = contextual_tool_calls or _build_surface_direct_tool_calls("home", trimmed_message, conversation, normalized_thread_state)
    direct_execution_tool_calls = _agent_tool_call_slice(direct_tool_calls)
    direct_guardrail = _home_requires_direct_guardrail(trimmed_message, normalized_message, direct_tool_calls)
    hard_direct_guardrail = _home_requires_hard_direct_execution(trimmed_message, normalized_message, direct_tool_calls)
    missing_direct_tool_scopes = _assistant_missing_scopes_for_tool_calls(session, direct_execution_tool_calls) if direct_tool_calls else []
    planner_preferred = _should_prefer_planner_for_home(
        trimmed_message,
        normalized_message,
        conversation,
        contextual_tool_calls=contextual_tool_calls,
        direct_tool_calls=direct_tool_calls,
    )
    direct_first = hard_direct_guardrail
    base_trace = {
        "surface": "home",
        "message": trimmed_message,
        "normalized_message": normalized_message,
        "grounded_request": grounded_request,
        "contextual_direct": bool(contextual_tool_calls),
        "direct_guardrail": direct_guardrail,
        "hard_direct_guardrail": hard_direct_guardrail,
        "planner_preferred": planner_preferred,
        "decision_mode": (
            "direct-hard-guardrail"
            if hard_direct_guardrail
            else "llm-planner-first"
        ),
        "direct_first": direct_first,
        "direct_tool_calls": _trace_tool_calls(direct_tool_calls),
        "missing_direct_tool_scopes": missing_direct_tool_scopes,
        "heuristic_missing_message_scopes": heuristic_missing_scopes_for_message,
        "thread_state_before": _snapshot_trace_value(normalized_thread_state),
    }

    try:
        if direct_tool_calls and missing_direct_tool_scopes and direct_first:
            return _assistant_scope_access_denied_run(
                normalized_thread_state=normalized_thread_state,
                message=trimmed_message,
                missing_scopes=missing_direct_tool_scopes,
            )
        if direct_first and direct_tool_calls:
            tool_results = []
            for tool_call in direct_execution_tool_calls:
                tool_results.append(await _execute_tool_call(session, tool_call, prior_tool_results=tool_results))
            next_thread_state = _derive_home_thread_state(
                previous_state=normalized_thread_state,
                message=trimmed_message,
                executed_tool_calls=direct_execution_tool_calls,
                tool_results=tool_results,
                route="direct-tool-execution",
                conversation=conversation,
            )
            reply, model = await _synthesize_from_tool_results(
                session=session,
                message=trimmed_message,
                conversation=conversation,
                tool_results=tool_results,
            )
            return OperationalAssistantRun(
                reply=reply,
                model=model,
                route="direct-tool-execution",
                trace={
                    **base_trace,
                    "executed_tool_results": _trace_tool_results(tool_results),
                    "thread_state_after": _snapshot_trace_value(next_thread_state),
                },
                thread_state=next_thread_state,
            )

        plan = await _plan_tool_usage(
            session,
            message=trimmed_message,
            conversation=conversation,
            thread_state=normalized_thread_state,
        )
        rejected_grounded_reply_plan: AssistantPlan | None = None
        if plan.mode == "reply" and grounded_request and not _is_capability_question(normalized_message):
            forced_plan = await _force_tool_plan_for_grounded_request(
                session=session,
                message=trimmed_message,
                conversation=conversation,
                thread_state=normalized_thread_state,
                rejected_reply=plan.reply,
            )
            if forced_plan is not None:
                rejected_grounded_reply_plan = plan
                plan = forced_plan
        if plan.mode == "reply":
            if not plan.reply.strip() and (direct_guardrail or grounded_request) and direct_tool_calls:
                tool_results = []
                for tool_call in direct_execution_tool_calls:
                    tool_results.append(await _execute_tool_call(session, tool_call, prior_tool_results=tool_results))
                next_thread_state = _derive_home_thread_state(
                    previous_state=normalized_thread_state,
                    message=trimmed_message,
                    executed_tool_calls=direct_execution_tool_calls,
                    tool_results=tool_results,
                    route="planner-fallback-direct-tools",
                    conversation=conversation,
                )
                reply, model = await _synthesize_from_tool_results(
                    session=session,
                    message=trimmed_message,
                    conversation=conversation,
                    tool_results=tool_results,
                )
                return OperationalAssistantRun(
                    reply=reply,
                    model=model,
                    route="planner-fallback-direct-tools",
                    trace={
                        **base_trace,
                        "planner": plan.model_dump(),
                        **(
                            {"rejected_grounded_reply_planner": rejected_grounded_reply_plan.model_dump()}
                            if rejected_grounded_reply_plan is not None
                            else {}
                        ),
                        "executed_tool_results": _trace_tool_results(tool_results),
                        "thread_state_after": _snapshot_trace_value(next_thread_state),
                    },
                    thread_state=next_thread_state,
                )
            next_thread_state = normalized_thread_state
            if _is_product_write_request(trimmed_message, normalized_message):
                next_thread_state = _derive_home_thread_state(
                    previous_state=normalized_thread_state,
                    message=trimmed_message,
                    executed_tool_calls=[],
                    tool_results=[],
                    route="deterministic-product-clarification",
                    conversation=conversation,
                )
            elif _is_sales_goal_write_request(normalized_message) or _conversation_suggests_pending_sales_goal_write(
                trimmed_message,
                conversation,
                normalized_thread_state,
            ):
                next_thread_state = _derive_home_thread_state(
                    previous_state=normalized_thread_state,
                    message=trimmed_message,
                    executed_tool_calls=[],
                    tool_results=[],
                    route="deterministic-sales-goal-clarification",
                    conversation=conversation,
                )
            return OperationalAssistantRun(
                reply=plan.reply.strip(),
                model="operational-planner",
                route="planner-reply",
                trace={
                    **base_trace,
                    "planner": plan.model_dump(),
                    **(
                        {"rejected_grounded_reply_planner": rejected_grounded_reply_plan.model_dump()}
                        if rejected_grounded_reply_plan is not None
                        else {}
                    ),
                    "thread_state_after": _snapshot_trace_value(next_thread_state),
                },
                thread_state=next_thread_state,
            )

        normalized_planned_tool_calls = _normalize_home_planned_tool_calls(trimmed_message, plan.tool_calls)
        normalized_planned_tool_calls = _apply_home_thread_state_to_tool_calls(
            trimmed_message,
            normalized_planned_tool_calls,
            conversation=conversation,
            thread_state=normalized_thread_state,
        )
        planned_execution_tool_calls = _agent_tool_call_slice(normalized_planned_tool_calls)
        missing_planned_tool_scopes = _assistant_missing_scopes_for_tool_calls(session, planned_execution_tool_calls)
        if missing_planned_tool_scopes:
            return _assistant_scope_access_denied_run(
                normalized_thread_state=normalized_thread_state,
                message=trimmed_message,
                missing_scopes=missing_planned_tool_scopes,
            )
        tool_results = []
        for tool_call in planned_execution_tool_calls:
            tool_results.append(await _execute_tool_call(session, tool_call, prior_tool_results=tool_results))
        if _tool_results_need_agent_retry(planned_execution_tool_calls, tool_results):
            retry_plan = await _replan_after_tool_results(
                session=session,
                message=trimmed_message,
                conversation=conversation,
                thread_state=normalized_thread_state,
                previous_tool_calls=planned_execution_tool_calls,
                previous_tool_results=tool_results,
            )
            if retry_plan is not None:
                if retry_plan.mode == "reply" and retry_plan.reply.strip():
                    if _should_fallback_to_direct_after_planner_results(
                        trimmed_message,
                        normalized_message,
                        tool_results,
                        direct_tool_calls,
                    ):
                        direct_tool_results = []
                        for tool_call in direct_execution_tool_calls:
                            direct_tool_results.append(await _execute_tool_call(session, tool_call, prior_tool_results=direct_tool_results))
                        next_thread_state = _derive_home_thread_state(
                            previous_state=normalized_thread_state,
                            message=trimmed_message,
                            executed_tool_calls=direct_execution_tool_calls,
                            tool_results=direct_tool_results,
                            route="planner-retry-empty-fallback-direct-tools",
                            conversation=conversation,
                        )
                        reply, model = await _synthesize_from_tool_results(
                            session=session,
                            message=trimmed_message,
                            conversation=conversation,
                            tool_results=direct_tool_results,
                        )
                        return OperationalAssistantRun(
                            reply=reply,
                            model=model,
                            route="planner-retry-empty-fallback-direct-tools",
                            trace={
                                **base_trace,
                                "planner": plan.model_dump(),
                                **(
                                    {"rejected_grounded_reply_planner": rejected_grounded_reply_plan.model_dump()}
                                    if rejected_grounded_reply_plan is not None
                                    else {}
                                ),
                                "normalized_planned_tool_calls": _trace_tool_calls(normalized_planned_tool_calls),
                                "executed_tool_results": _trace_tool_results(tool_results),
                                "retry_planner": retry_plan.model_dump(),
                                "fallback_tool_results": _trace_tool_results(direct_tool_results),
                                "thread_state_after": _snapshot_trace_value(next_thread_state),
                            },
                            thread_state=next_thread_state,
                        )
                    next_thread_state = _derive_home_thread_state(
                        previous_state=normalized_thread_state,
                        message=trimmed_message,
                        executed_tool_calls=planned_execution_tool_calls,
                        tool_results=tool_results,
                        route="planner-retry-reply",
                        conversation=conversation,
                    )
                    return OperationalAssistantRun(
                        reply=retry_plan.reply.strip(),
                        model="operational-planner",
                        route="planner-retry-reply",
                        trace={
                            **base_trace,
                            "planner": plan.model_dump(),
                            **(
                                {"rejected_grounded_reply_planner": rejected_grounded_reply_plan.model_dump()}
                                if rejected_grounded_reply_plan is not None
                                else {}
                            ),
                            "normalized_planned_tool_calls": _trace_tool_calls(normalized_planned_tool_calls),
                            "executed_tool_results": _trace_tool_results(tool_results),
                            "retry_planner": retry_plan.model_dump(),
                            "thread_state_after": _snapshot_trace_value(next_thread_state),
                        },
                        thread_state=next_thread_state,
                    )
                if retry_plan.mode == "tool" and _is_agent_read_only_plan(retry_plan.tool_calls):
                    retry_tool_calls = _normalize_home_planned_tool_calls(trimmed_message, retry_plan.tool_calls)
                    retry_tool_calls = _apply_home_thread_state_to_tool_calls(
                        trimmed_message,
                        retry_tool_calls,
                        conversation=conversation,
                        thread_state=normalized_thread_state,
                    )
                    retry_execution_tool_calls = _agent_tool_call_slice(retry_tool_calls)
                    missing_retry_tool_scopes = _assistant_missing_scopes_for_tool_calls(session, retry_execution_tool_calls)
                    if missing_retry_tool_scopes:
                        return _assistant_scope_access_denied_run(
                            normalized_thread_state=normalized_thread_state,
                            message=trimmed_message,
                            missing_scopes=missing_retry_tool_scopes,
                        )
                    retry_tool_results = []
                    for tool_call in retry_execution_tool_calls:
                        retry_tool_results.append(await _execute_tool_call(session, tool_call, prior_tool_results=retry_tool_results))
                    if not _tool_results_need_agent_retry(retry_execution_tool_calls, retry_tool_results):
                        next_thread_state = _derive_home_thread_state(
                            previous_state=normalized_thread_state,
                            message=trimmed_message,
                            executed_tool_calls=retry_execution_tool_calls,
                            tool_results=retry_tool_results,
                            route="planner-retry-tool-execution",
                            conversation=conversation,
                        )
                        reply, model = await _synthesize_from_tool_results(
                            session=session,
                            message=trimmed_message,
                            conversation=conversation,
                            tool_results=retry_tool_results,
                        )
                        return OperationalAssistantRun(
                            reply=reply,
                            model=model,
                            route="planner-retry-tool-execution",
                            trace={
                                **base_trace,
                                "planner": plan.model_dump(),
                                **(
                                    {"rejected_grounded_reply_planner": rejected_grounded_reply_plan.model_dump()}
                                    if rejected_grounded_reply_plan is not None
                                    else {}
                                ),
                                "normalized_planned_tool_calls": _trace_tool_calls(normalized_planned_tool_calls),
                                "executed_tool_results": _trace_tool_results(tool_results),
                                "retry_planner": retry_plan.model_dump(),
                                "retry_tool_results": _trace_tool_results(retry_tool_results),
                                "thread_state_after": _snapshot_trace_value(next_thread_state),
                            },
                            thread_state=next_thread_state,
                        )
        if _should_fallback_to_direct_after_planner_results(
            trimmed_message,
            normalized_message,
            tool_results,
            direct_tool_calls,
        ):
            direct_tool_results = []
            for tool_call in direct_execution_tool_calls:
                direct_tool_results.append(await _execute_tool_call(session, tool_call, prior_tool_results=direct_tool_results))
            next_thread_state = _derive_home_thread_state(
                previous_state=normalized_thread_state,
                message=trimmed_message,
                executed_tool_calls=direct_execution_tool_calls,
                tool_results=direct_tool_results,
                route="planner-empty-fallback-direct-tools",
                conversation=conversation,
            )
            reply, model = await _synthesize_from_tool_results(
                session=session,
                message=trimmed_message,
                conversation=conversation,
                tool_results=direct_tool_results,
            )
            return OperationalAssistantRun(
                reply=reply,
                model=model,
                route="planner-empty-fallback-direct-tools",
                trace={
                    **base_trace,
                    "planner": plan.model_dump(),
                    **(
                        {"rejected_grounded_reply_planner": rejected_grounded_reply_plan.model_dump()}
                        if rejected_grounded_reply_plan is not None
                        else {}
                    ),
                    "normalized_planned_tool_calls": _trace_tool_calls(normalized_planned_tool_calls),
                    "planner_tool_results": _trace_tool_results(tool_results),
                    "executed_tool_results": _trace_tool_results(direct_tool_results),
                    "thread_state_after": _snapshot_trace_value(next_thread_state),
                },
                thread_state=next_thread_state,
            )
        next_thread_state = _derive_home_thread_state(
            previous_state=normalized_thread_state,
            message=trimmed_message,
            executed_tool_calls=planned_execution_tool_calls,
            tool_results=tool_results,
            route="planner-tool-execution",
            conversation=conversation,
        )

        reply, model = await _synthesize_from_tool_results(
            session=session,
            message=trimmed_message,
            conversation=conversation,
            tool_results=tool_results,
        )
        return OperationalAssistantRun(
            reply=reply,
            model=model,
            route="planner-tool-execution",
            trace={
                **base_trace,
                "planner": plan.model_dump(),
                **(
                    {"rejected_grounded_reply_planner": rejected_grounded_reply_plan.model_dump()}
                    if rejected_grounded_reply_plan is not None
                    else {}
                ),
                "normalized_planned_tool_calls": _trace_tool_calls(normalized_planned_tool_calls),
                "executed_tool_results": _trace_tool_results(tool_results),
                "thread_state_after": _snapshot_trace_value(next_thread_state),
            },
            thread_state=next_thread_state,
        )
    except (HTTPException, ValueError) as exc:
        if direct_tool_calls:
            try:
                if missing_direct_tool_scopes:
                    return _assistant_scope_access_denied_run(
                        normalized_thread_state=normalized_thread_state,
                        message=trimmed_message,
                        missing_scopes=missing_direct_tool_scopes,
                    )
                tool_results = []
                for tool_call in direct_execution_tool_calls:
                    tool_results.append(await _execute_tool_call(session, tool_call, prior_tool_results=tool_results))
                next_thread_state = _derive_home_thread_state(
                    previous_state=normalized_thread_state,
                    message=trimmed_message,
                    executed_tool_calls=direct_execution_tool_calls,
                    tool_results=tool_results,
                    route="exception-recovery-direct-tools",
                    conversation=conversation,
                )
                reply, model = await _synthesize_from_tool_results(
                    session=session,
                    message=trimmed_message,
                    conversation=conversation,
                    tool_results=tool_results,
                )
                return OperationalAssistantRun(
                    reply=reply,
                    model=model,
                    route="exception-recovery-direct-tools",
                    trace={
                        **base_trace,
                        "error_detail": _humanize_operational_error_detail(exc.detail if isinstance(exc, HTTPException) and isinstance(exc.detail, str) else str(exc)),
                        "executed_tool_results": _trace_tool_results(tool_results),
                        "thread_state_after": _snapshot_trace_value(next_thread_state),
                    },
                    thread_state=next_thread_state,
                )
            except Exception:
                pass
        detail = exc.detail if isinstance(exc, HTTPException) else str(exc)
        safe_detail = _humanize_operational_error_detail(detail if isinstance(detail, str) else None)
        return OperationalAssistantRun(
            reply=f"Non riesco ancora a leggere o aggiornare questo dato operativo in modo affidabile. {safe_detail}. Prova a riformulare la richiesta in modo piu specifico.",
            model="operational-safe-fallback",
            route="safe-fallback",
            trace={**base_trace, "error_detail": safe_detail},
            thread_state=normalized_thread_state,
        )


async def run_operational_assistant(
    *,
    session: SessionIdentity,
    message: str,
    conversation: list[dict[str, str]],
    thread_state: dict[str, object] | None = None,
) -> tuple[str, str]:
    outcome = await run_operational_assistant_with_trace(
        session=session,
        message=message,
        conversation=conversation,
        thread_state=thread_state,
    )
    return outcome.reply, outcome.model
