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


_STOPWORDS = {
    "a",
    "ad",
    "ai",
    "al",
    "alla",
    "allo",
    "che",
    "ceh",
    "chi",
    "ci",
    "coi",
    "con",
    "da",
    "dal",
    "dalla",
    "dei",
    "degli",
    "del",
    "della",
    "delle",
    "dello",
    "di",
    "e",
    "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",
    "mostri",
    "mostra",
    "mostrami",
    "prezzo",
    "prezzi",
    "ultimo",
    "ultimi",
    "effettuato",
    "effettuati",
    "per",
    "acquistiamo",
    "acquisti",
    "comprare",
    "compriamo",
    "quale",
    "quali",
    "quanti",
    "quanto",
    "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",
}

_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",
)
_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_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",
        "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",
    "agosto",
    "ai",
    "al",
    "alla",
    "alle",
    "allo",
    "anno",
    "aprile",
    "avete",
    "cl",
    "confronta",
    "confrontare",
    "confronto",
    "cose",
    "cosa",
    "cerca",
    "cercare",
    "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",
    "nelle",
    "negli",
    "nel",
    "nella",
    "nello",
    "novembre",
    "ordinare",
    "ordinata",
    "ordinato",
    "ordinati",
    "ordine",
    "ordini",
    "ottobre",
    "passata",
    "passato",
    "periodi",
    "periodo",
    "pezzi",
    "pezzo",
    "piu",
    "prodotti",
    "prodotto",
    "quante",
    "quanti",
    "quanto",
    "quantita",
    "riga",
    "righe",
    "rispetto",
    "ritorna",
    "ritornami",
    "scorsa",
    "scorso",
    "settembre",
    "solo",
    "unit",
    "units",
    "pack",
    "packs",
    "parlando",
    "percentuale",
    "percentuali",
    "contiene",
    "contengono",
    "sospeso",
    "storico",
    "tra",
    "totale",
    "totali",
    "trova",
    "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",
    "compra",
    "comprare",
    "comprensiva",
    "comprensive",
    "comprensivi",
    "comprensivo",
    "comprata",
    "comprate",
    "comprati",
    "comprato",
    "costosa",
    "costose",
    "costosi",
    "costoso",
    "economica",
    "economiche",
    "economici",
    "economico",
    "elenca",
    "elencami",
    "elenco",
    "fatta",
    "fatte",
    "fatti",
    "fatto",
    "lista",
    "non",
    "ordina",
    "ordinano",
    "ordinate",
    "ordinato",
    "ordiniamo",
    "ordino",
    "penultima",
    "penultime",
    "penultimi",
    "penultimo",
    "prezzo",
    "prezzi",
    "proditti",
    "prima",
    "seconda",
    "terza",
    "quarta",
    "quinta",
    "separata",
    "separate",
    "separati",
    "separato",
    "settimana",
    "settimane",
    "ultima",
    "ultime",
    "ultimi",
    "ultimo",
    "voglio",
    "vorrei",
    "speso",
    "spesa",
    "spendere",
    "soldi",
    "euro",
    "eur",
    "importo",
    "importi",
    "valore",
    "valori",
    "costo",
    "costi",
}
_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",
        "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)

    @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
    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 = Field(default=_CHAT_LIST_LIMIT, ge=1, le=_CHAT_LIST_LIMIT)
    include_entries: bool = False


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)
    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(("a", "e", "i", "o")):
        return cleaned[:-1]
    return cleaned


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 _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|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 _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 _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 _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 _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_permission(session, permission)


def _is_timeclock_request(normalized: str) -> bool:
    return any(keyword in normalized for keyword in _TIMECLOCK_KEYWORDS)


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


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")
            _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, "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, "shared_notes", shared_note_rows)
            _create_query_table_from_rows(connection, "suspended_orders", suspended_order_rows)

    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, "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_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.",
        ],
        "example_queries": [
            "SELECT supplier_name, COUNT(*) AS products FROM ordini_products GROUP BY supplier_name ORDER BY products DESC LIMIT 10",
            "SELECT document_type, supplier_name, total_amount FROM tenant_fiscal_documents ORDER BY document_date DESC LIMIT 20",
            "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",
        ],
    }


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()
    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,
            )
        )

    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_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_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_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 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_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": [],
                "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"]))
        batch["items"].append(
            {
                "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,
            }
        )
        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,
                "match_score": round(score, 3),
            }
        )

    if args.query.strip():
        selected_batches.sort(key=lambda batch: str(batch["confirmed_at"] or ""), reverse=True)
        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=True)
    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,
        "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_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 _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 == "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)
    return None


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)
    wants_latest_batches = _is_latest_batches_request(normalized)
    latest_batches_limit = _extract_requested_latest_batches_limit(message)
    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 (
        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 explicit_year is None
        and explicit_month is None
    ):
        return tool_calls

    normalized_calls: list[PlannedToolCall] = []
    changed = False
    for tool_call in tool_calls:
        arguments = dict(tool_call.arguments)
        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":
            arguments["query"] = purchase_query
            arguments["limit"] = latest_batches_limit
            if explicit_year is None and explicit_month is None and week_range is None:
                arguments["year"] = None
                arguments["month"] = None
                arguments["start_date"] = None
                arguments["end_date"] = None
            normalized_calls.append(PlannedToolCall(tool="get_purchase_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 wants_order_batches and tool_call.tool == "get_purchase_batches":
            arguments["query"] = purchase_query if wants_latest_batches else str(arguments.get("query") or "")
            if wants_latest_batches and explicit_year is None and explicit_month is None and week_range is None:
                arguments["year"] = None
                arguments["month"] = None
            else:
                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"] = latest_batches_limit if wants_latest_batches else _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":
            arguments["query"] = purchase_query if wants_latest_batches else str(arguments.get("query") or "")
            if wants_latest_batches and explicit_year is None and explicit_month is None and week_range is None:
                arguments["year"] = None
                arguments["month"] = None
            else:
                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"] = latest_batches_limit if wants_latest_batches else _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_history":
            arguments["query"] = purchase_query if wants_latest_batches else str(arguments.get("query") or "")
            if wants_latest_batches and explicit_year is None and explicit_month is None and week_range is None:
                arguments["year"] = None
                arguments["month"] = None
            else:
                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"] = latest_batches_limit if wants_latest_batches else _CHAT_LIST_LIMIT
            normalized_calls.append(PlannedToolCall(tool="get_purchase_batches", arguments=arguments))
            changed = True
            continue

        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.",
        }

    enriched_prompt = _compose_document_generation_prompt(
        base_prompt=prompt,
        prior_tool_results=prior_tool_results or [],
    )
    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_tool_results or [],
    )
    if grounded_preview is not None:
        preview = grounded_preview
    else:
        prompt_grounded_preview = _build_grounded_purchase_document_preview_from_prompt(session, args)
        if prompt_grounded_preview is not None:
            preview = prompt_grounded_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)
    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=args.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 = summary_rows[0] if len(summary_rows) == 1 else 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 _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 == "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 _planner_system_prompt(session: SessionIdentity) -> str:
    context = _get_locale_profile(session)
    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, 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.",
            "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(),
                "limit": _CHAT_LIST_LIMIT,
                "include_entries": False,
            },
        },
        {
            "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.",
            "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.",
            "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 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.",
            "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.",
            "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.",
            "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 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 usa sempre date esplicite nel formato YYYY-MM-DD.",
            "Per ordini sospesi: 'crea' normalmente -> operation='set'; 'aggiungi' -> operation='add'.",
            f"Data e ora correnti: {now.isoformat()} ({get_settings().assistant_timezone}).",
            f"Locale attivo: {json.dumps(context, ensure_ascii=False)}",
            f"Tool disponibili: {json.dumps(tool_descriptions, ensure_ascii=False)}",
            'Restituisci SOLO JSON con questa forma: {"mode":"reply","reply":"...","tool_calls":[]} oppure {"mode":"tool","reply":null,"tool_calls":[{"tool":"nome_tool","arguments":{...}}]}.',
        ]
    )


def _build_recent_conversation(conversation: list[dict[str, str]]) -> str:
    if not conversation:
        return "(nessun messaggio precedente)"
    lines = []
    for message in conversation[-6:]:
        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",
        "last_tool",
        "purchase_first_ordered_at",
        "purchase_last_ordered_at",
    ):
        value = thread_state.get(key)
        if isinstance(value, str) and value.strip():
            state[key] = value.strip()

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

    for key in ("purchase_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"):
        value = _coerce_optional_iso_date(thread_state.get(key))
        if value is not None:
            state[key] = value

    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"]
    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 _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)
    latest_batches_limit = _extract_requested_latest_batches_limit(message)
    should_inherit_purchase = _should_inherit_purchase_context(message, normalized)
    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()

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

        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

    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:
                state["catalog_query"] = query
            state["last_tool"] = "search_products"
        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 == "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)
    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]] = []

    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 ("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_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 _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 ("ordine", "ordini", "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",
        "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 _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 None


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")
    )


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", "staff", "dipendent"))
        and any(keyword in normalized for keyword in ("quali", "lista", "elenco", "mostra", "mostrami", "vedere", "leggi", "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_timeclock_request(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", "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_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"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:
                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_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))
            ):
                query = _extract_catalog_subject_query(content)
                if query:
                    return query
        elif role == "assistant":
            query = _extract_catalog_subject_from_assistant_message(content)
            if 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

    query_tokens = [
        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
    ]
    if len(query_tokens) < 2:
        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 ""),
                ],
            )
        )
        searchable_tokens = _tokenize_query(searchable)
        if all(any(_tokens_match(query_token, searchable_token) for searchable_token in searchable_tokens) for query_token in 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

    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 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|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)\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", "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
    reduced = normalized
    for token in ("document", "documento", "documenti", "google doc", "doc", "foglio", "sheet", "file", "crea", "genera", "prepara", "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_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, ordini storici, prenotazioni, note, obiettivi e documenti, "
            "ma non ho ancora dati reali di venduto o incasso."
        )
    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_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 = {
        "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 _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 _contains_goal_keyword(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 _contains_goal_keyword(_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 _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 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 _build_contextual_tool_calls(
    message: str,
    conversation: list[dict[str, str]],
    thread_state: dict[str, object] | None = None,
) -> list[PlannedToolCall]:
    reservation_tool_calls = _build_contextual_reservation_create_tool_calls(message, conversation)
    if reservation_tool_calls:
        return reservation_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

    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_tool_calls = _build_contextual_sales_goal_write_tool_calls(message, conversation, thread_state)
    if sales_goal_tool_calls:
        return sales_goal_tool_calls

    last_user_message = _latest_user_message(conversation)
    if not last_user_message:
        return []

    normalized_previous = _normalize_text(last_user_message)
    normalized_current = _normalize_text(message)
    state = _normalize_home_thread_state(thread_state)
    previous_query = _extract_purchase_query(last_user_message) or str(state.get("purchase_query") or "")
    current_query = _extract_purchase_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"))
    previous_catalog_query = str(state.get("catalog_query") or "") or _latest_catalog_subject_query(conversation)

    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)
    ):
        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:
    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 == "get_timeclock_summary" 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 _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 not direct_tool_calls:
        return False
    if all(tool_call.tool == "get_timeclock_summary" for tool_call in 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 _home_requires_direct_guardrail(message, normalized, direct_tool_calls):
        return False
    if grounded_request := _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:
    scope: Literal["today", "week", "all", "active"] = "today"
    if (
        _contains_normalized_word(normalized, "adesso", "ora")
        or any(fragment in normalized for fragment in ("in turno", "turni attivi", "attivo"))
    ):
        scope = "active"
    elif "settiman" in normalized:
        scope = "week"
    elif any(fragment in normalized for fragment in ("storico completo", "tutto il cartellino", "tutti i turni")):
        scope = "all"

    target_date = _extract_explicit_date(message)
    if target_date is None:
        if "ieri" in normalized:
            target_date = _today_in_timezone() - timedelta(days=1)
        elif "domani" in normalized and scope != "active":
            target_date = _today_in_timezone() + timedelta(days=1)
        elif "oggi" in normalized and scope not in {"active", "week"}:
            target_date = _today_in_timezone()

    include_entries = any(fragment in normalized for fragment in ("timbrature", "dettaglio", "dettagli", "mostra", "mostrami", "elenco", "storico"))

    return PlannedToolCall(
        tool="get_timeclock_summary",
        arguments={
            "query_text": message,
            "scope": scope,
            "target_date": target_date.isoformat() if target_date is not None and scope not in {"active", "week"} else None,
            "limit": _CHAT_LIST_LIMIT,
            "include_entries": include_entries,
        },
    )


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)
    if _is_timeclock_request(normalized):
        return [_build_timeclock_summary_tool_call(message, normalized)]
    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_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_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})]

    if _is_tenant_user_list_request(normalized):
        return [PlannedToolCall(tool="list_tenant_users", arguments={})]

    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 "obiettiv" in normalized and any(keyword in normalized for keyword in ("mostra", "mostrami", "lista", "elenca", "vedere", "leggi", "quali")):
        year = _extract_explicit_year(message)
        return [PlannedToolCall(tool="get_sales_goals", arguments={"year": year})]

    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(
                [
                    "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)
    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(
                    [
                        "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)
        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 _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 _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 _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 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_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)

        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 = "Oggi" if scope == "today" else "Questa settimana" if scope == "week" else "Nel periodo richiesto"
                lines = [f"{prefix} hai lavorato {total_hours}."]
            else:
                prefix = "Oggi" if scope == "today" else "Questa settimana" if scope == "week" else "Nel periodo richiesto"
                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:
            lines = [f"Riepilogo cartellino per {scope_label}:"]
            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 "Non vedo timbrature per oggi."
        if scope == "week":
            return "Non vedo timbrature per questa settimana."
        return "Non vedo timbrature nel perimetro richiesto."

    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."
        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 == "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:
            lines = [f"Nel catalogo prodotti del locale risultano {total_count} articoli riconducibili alla famiglia {request_label}:"]
        elif narrowed_focus:
            lines = [f"Nel catalogo prodotti del locale trovo {len(display_items)} varianti rilevanti per {catalog_query or request_label}:"]
        else:
            lines = [f"Nel catalogo prodotti del locale risultano {total_count} articoli che corrispondono 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."
            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."
            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):
                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 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)
        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)

    return None


def _final_system_prompt(session: SessionIdentity) -> str:
    context = _get_locale_profile(session)
    now = _now_in_timezone()
    return "\n".join(
        [
            "Sei l'assistente operativo globale del locale.",
            "Rispondi sempre in italiano.",
            "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.",
            f"Data e ora correnti: {now.isoformat()} ({get_settings().assistant_timezone}).",
            f"Contesto locale: {json.dumps(context, ensure_ascii=False)}",
        ]
    )


def _build_tool_results_payload(tool_results: list[dict[str, object]]) -> str:
    return json.dumps(tool_results, ensure_ascii=False, default=str)


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


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:
        return deterministic_reply, "operational-deterministic"

    synthesis_messages = [
        {"role": "system", "content": _final_system_prompt(session)},
        {
            "role": "user",
            "content": "\n".join(
                [
                    "Conversazione recente:",
                    _build_recent_conversation(conversation),
                    "",
                    f"Messaggio utente attuale: {message}",
                    "",
                    "Risultati tool reali:",
                    _build_tool_results_payload(tool_results),
                ]
            ),
        },
    ]

    reply, model = await request_llm_chat_completion(synthesis_messages, temperature=0.1)
    return reply.strip(), model


def _timeclock_access_denied_run(*, normalized_thread_state: dict[str, object], message: str) -> OperationalAssistantRun:
    return OperationalAssistantRun(
        reply="Non possiedi l'autorizzazione per usare Cartellino. 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,
    )


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)
    if _is_timeclock_request(normalized_message) and not get_tenant_store().session_has_permission(session, "timeclock"):
        return _timeclock_access_denied_run(normalized_thread_state=normalized_thread_state, message=trimmed_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,
        )

    capability_reply = _build_capability_reply(trimmed_message, normalized_message)
    if capability_reply:
        return OperationalAssistantRun(
            reply=capability_reply,
            model="operational-deterministic",
            route="deterministic-capability",
            trace={"surface": "home", "message": trimmed_message, "normalized_message": normalized_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_guardrail = _home_requires_direct_guardrail(trimmed_message, normalized_message, direct_tool_calls)
    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 = (
        direct_guardrail
        or (bool(contextual_tool_calls) and not planner_preferred)
        or (grounded_request and bool(direct_tool_calls) and not planner_preferred)
    )
    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,
        "planner_preferred": planner_preferred,
        "direct_first": direct_first,
        "direct_tool_calls": _trace_tool_calls(direct_tool_calls),
        "thread_state_before": _snapshot_trace_value(normalized_thread_state),
    }

    try:
        if direct_first and direct_tool_calls:
            tool_results = []
            for tool_call in direct_tool_calls[:3]:
                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_tool_calls[:3],
                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,
        )
        if plan.mode == "reply":
            if direct_guardrail and direct_tool_calls:
                tool_results = []
                for tool_call in direct_tool_calls[:3]:
                    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_tool_calls[:3],
                    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(),
                        "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(),
                    "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,
        )
        tool_results = []
        for tool_call in normalized_planned_tool_calls[:3]:
            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=normalized_planned_tool_calls[:3],
            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(),
                "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:
                tool_results = []
                for tool_call in direct_tool_calls[:3]:
                    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_tool_calls[:3],
                    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
