"""Natural Language Query parser — regex-based, no external dependencies.

Translates free-text queries (PT-BR, EN, ES) into Odoo search domains.
"""

from __future__ import annotations

import re
import calendar
from dataclasses import dataclass
from datetime import datetime, date

from cli_anything.odoo_12.core import aliases


@dataclass
class ExportIntent:
    format: str  # "csv" | "json"
    path: str


@dataclass
class ParsedQuery:
    model: str
    domain: list
    fields: list[str]
    limit: int = 50
    title: str = ""
    export: ExportIntent | None = None
    raw_query: str = ""
    sum_field: str | None = None
    group_by: str | None = None       # simple field name to group results by
    cross_group_key: str | None = None  # cross-model group key (e.g. "pos_categ")


# ── Regex Patterns ────────────────────────────────────────────

_RE_EXPORT = re.compile(
    r"(?:export[ea]?|exporte)\s+(?:para|to)\s+(csv|json)\s+(?:em|in|at)\s+[\"']?([^\s\"']+)[\"']?",
    re.IGNORECASE,
)

_RE_LIMIT = re.compile(
    r"(?:limit[ea]?|limitar?\s+a?|top|primeiro[sa]?|ultimos?|últimos?)\s+(\d+)",
    re.IGNORECASE,
)

# Month/year: "em janeiro/2024", "em march 2024", "in january/2024"
_MONTH_PATTERN = "|".join(sorted(aliases.MONTH_NAMES.keys(), key=len, reverse=True))
_RE_MONTH_YEAR = re.compile(
    rf"(?:em|in|de|en)\s+({_MONTH_PATTERN})[/\-\s]?(\d{{4}})?",
    re.IGNORECASE,
)

# Date range: "de 01/03 a 15/03", "from 01/03 to 15/03"
_RE_DATE_RANGE = re.compile(
    r"(?:de|from)\s+(\d{1,2}[/\-]\d{1,2}(?:[/\-]\d{2,4})?)\s+(?:a|at[eé]|to)\s+(\d{1,2}[/\-]\d{1,2}(?:[/\-]\d{2,4})?)",
    re.IGNORECASE,
)

# Relative dates
_RE_THIS_MONTH = re.compile(
    r"(?:este\s+m[eê]s|this\s+month|este\s+mes)",
    re.IGNORECASE,
)
_RE_LAST_MONTH = re.compile(
    r"(?:[uú]ltimo\s+m[eê]s|last\s+month|mes\s+pasado)",
    re.IGNORECASE,
)

# Birthday: "aniversariantes de março", "birthdays in march"
_RE_BIRTHDAY = re.compile(
    rf"(?:anivers[aá]ri(?:o|antes?)|birthdays?)\s+(?:de|em|in)\s+({_MONTH_PATTERN})",
    re.IGNORECASE,
)

# CPF/CNPJ
_RE_CPF_CNPJ = re.compile(
    r"(?:cpf|cnpj)\s*:?\s*([\d.\-/]+)",
    re.IGNORECASE,
)

# Amount: "acima de R$ 10.000", "above 5000", "> 1000"
_RE_AMOUNT = re.compile(
    r"(?:acima\s+de|maior\s+que|above|more\s+than|greater\s+than|>)\s*R?\$?\s*([\d.,]+)",
    re.IGNORECASE,
)
_RE_AMOUNT_BELOW = re.compile(
    r"(?:abaixo\s+de|menor\s+que|below|less\s+than|<)\s*R?\$?\s*([\d.,]+)",
    re.IGNORECASE,
)

# Group by: "agrupado por X", "grouped by X", "por categoria", "por ponto de venda"
_RE_GROUP_BY = re.compile(
    r"(?:agrupado?|grouped?|separado?)\s+(?:por|by)\s+(.+?)(?:\s+e\s+|\s*$)",
    re.IGNORECASE,
)
_RE_GROUP_BY_POR = re.compile(
    r"(?:por)\s+(categoria|ponto\s+de\s+venda|pdv|equipe|team|config|parceiro|partner|cliente|customer|estado|state|status)",
    re.IGNORECASE,
)

# Maps group-by keywords to field names
_GROUP_BY_KEYWORDS: dict[str, str] = {
    "categoria": "config_id",
    "ponto de venda": "config_id",
    "pdv": "config_id",
    "config": "config_id",
    "equipe": "team_id",
    "team": "team_id",
    "parceiro": "partner_id",
    "partner": "partner_id",
    "cliente": "partner_id",
    "customer": "partner_id",
    "estado": "state",
    "state": "state",
    "status": "state",
}
_SORTED_GROUP_KEYWORDS = sorted(_GROUP_BY_KEYWORDS.keys(), key=len, reverse=True)


# ── Helpers ───────────────────────────────────────────────────

def _parse_amount(text: str) -> float:
    """Parse amount string handling BR (10.000,50) and EN (10,000.50) formats.

    Heuristic:
    - If comma is the last separator and followed by 1-2 digits → BR format
    - If dot is used as thousands separator (groups of 3) → BR format
    - Otherwise → EN format
    """
    text = text.strip().replace(" ", "")
    if not text:
        return 0.0

    comma_pos = text.rfind(",")
    dot_pos = text.rfind(".")

    if comma_pos > dot_pos and comma_pos >= len(text) - 3:
        # BR format: 10.000,50 → remove dots, replace comma with dot
        text = text.replace(".", "").replace(",", ".")
    elif dot_pos != -1 and comma_pos == -1:
        # Could be BR thousands (10.000) or EN decimal (10.50)
        # If digits after dot form groups of 3 → BR thousands separator
        after_dot = text[dot_pos + 1:]
        if len(after_dot) == 3 and after_dot.isdigit():
            # BR thousands: 5.000 → 5000, 10.000 → 10000
            text = text.replace(".", "")
        # else: EN decimal like 10.50 — leave as is
    else:
        # EN format: 10,000.50 → remove commas
        text = text.replace(",", "")

    return float(text)


def _month_range(month: int, year: int) -> tuple[str, str]:
    """Return (first_day, last_day) as 'YYYY-MM-DD' strings."""
    last_day = calendar.monthrange(year, month)[1]
    return (
        f"{year:04d}-{month:02d}-01",
        f"{year:04d}-{month:02d}-{last_day:02d}",
    )


def _parse_date(date_str: str, default_year: int | None = None) -> date:
    """Parse a date string like '01/03', '01/03/2024', '01-03-2024'."""
    parts = re.split(r"[/\-]", date_str)
    day = int(parts[0])
    month = int(parts[1])
    if len(parts) >= 3:
        year = int(parts[2])
        if year < 100:
            year += 2000
    else:
        year = default_year or datetime.now().year
    return date(year, month, day)


# ── Title formatting ──────────────────────────────────────────

_LOWERCASE_WORDS = {
    "de", "do", "da", "dos", "das", "em", "no", "na", "nos", "nas",
    "o", "a", "os", "as", "e", "ou", "com", "por", "para", "que",
    "of", "the", "in", "on", "at", "to", "for", "and", "or", "by", "with",
}
_UPPERCASE_WORDS = {"pos", "pdv", "erp", "csv", "json", "cpf", "cnpj", "nfe"}


def _smart_title(text: str) -> str:
    """Title-case with Portuguese/English prepositions lowercase and acronyms uppercase."""
    words = text.split()
    result = []
    for i, w in enumerate(words):
        wl = w.lower()
        if wl in _UPPERCASE_WORDS:
            result.append(w.upper())
        elif i > 0 and wl in _LOWERCASE_WORDS:
            result.append(wl)
        else:
            result.append(w.capitalize())
    return " ".join(result)


# ── Main Parser ───────────────────────────────────────────────

def parse(query: str) -> ParsedQuery:
    """Parse a natural language query into a ParsedQuery.

    Pipeline:
    1. Export intent
    2. Limit
    3. Model resolution
    4. Date filters (month/year, range, relative, birthday)
    5. Status, Type, Boolean filters
    6. CPF/CNPJ
    7. Amount filters
    8. Compose title
    """
    raw = query.strip()
    domain: list = []
    title_parts: list[str] = []
    export_intent: ExportIntent | None = None
    limit = 50
    birthday_month: int | None = None

    # 1. Export
    m = _RE_EXPORT.search(raw)
    if m:
        export_intent = ExportIntent(format=m.group(1).lower(), path=m.group(2))

    # 2. Limit
    m = _RE_LIMIT.search(raw)
    if m:
        limit = int(m.group(1))

    # 3. Model
    resolved = aliases.resolve_model(raw)
    if resolved is None:
        supported = sorted(set(
            f"{a} → {m}" for a, (m, _) in aliases.MODEL_ALIASES.items()
        ))
        raise ValueError(
            f"Modelo não reconhecido na query: '{raw}'\n"
            f"Aliases suportados:\n  " + "\n  ".join(supported[:20])
        )

    model, fields, matched_alias, implicit_domain = resolved
    domain.extend(implicit_domain)
    title_parts.append(_smart_title(matched_alias))

    # 4a. Birthday
    m = _RE_BIRTHDAY.search(raw)
    if m:
        month_name = m.group(1).lower()
        birthday_month = aliases.MONTH_NAMES.get(month_name)
        if birthday_month:
            month_name_display = month_name.capitalize()
            title_parts.append(f"Aniversariantes de {month_name_display}")
            # We'll need to filter client-side; add domain to exclude empty birthday
            domain.append(("birthday", "!=", False))

    # 4b. Month/Year
    if birthday_month is None:
        m = _RE_MONTH_YEAR.search(raw)
        if m:
            month_name = m.group(1).lower()
            month_num = aliases.MONTH_NAMES.get(month_name)
            year = int(m.group(2)) if m.group(2) else datetime.now().year
            if month_num:
                date_field = aliases.DATE_FIELDS.get(model, "create_date")
                start, end = _month_range(month_num, year)
                domain.append((date_field, ">=", start))
                domain.append((date_field, "<=", end))
                title_parts.append(f"{month_name.capitalize()}/{year}")

    # 4c. Date range
    m = _RE_DATE_RANGE.search(raw)
    if m:
        try:
            d1 = _parse_date(m.group(1))
            d2 = _parse_date(m.group(2))
            date_field = aliases.DATE_FIELDS.get(model, "create_date")
            domain.append((date_field, ">=", d1.isoformat()))
            domain.append((date_field, "<=", d2.isoformat()))
            title_parts.append(f"{d1.strftime('%d/%m')} a {d2.strftime('%d/%m')}")
        except (ValueError, IndexError):
            pass

    # 4d. Relative dates
    if _RE_THIS_MONTH.search(raw):
        now = datetime.now()
        date_field = aliases.DATE_FIELDS.get(model, "create_date")
        start, end = _month_range(now.month, now.year)
        domain.append((date_field, ">=", start))
        domain.append((date_field, "<=", end))
        title_parts.append("Este Mês")
    elif _RE_LAST_MONTH.search(raw):
        now = datetime.now()
        if now.month == 1:
            lm, ly = 12, now.year - 1
        else:
            lm, ly = now.month - 1, now.year
        date_field = aliases.DATE_FIELDS.get(model, "create_date")
        start, end = _month_range(lm, ly)
        domain.append((date_field, ">=", start))
        domain.append((date_field, "<=", end))
        title_parts.append("Último Mês")

    # 5. Status
    status = aliases.get_status_clause(raw)
    if status:
        domain.append(status)
        title_parts.append(_smart_title(status[2]))

    # 6. Type (invoice direction)
    type_clause = aliases.get_type_clause(raw)
    if type_clause:
        domain.append(type_clause)
        # Friendly label
        type_labels = {"in_invoice": "de Entrada", "out_invoice": "de Saída"}
        title_parts.append(type_labels.get(type_clause[2], type_clause[2]))

    # 7. Boolean filters
    bool_clauses = aliases.get_boolean_clauses(raw)
    domain.extend(bool_clauses)

    # 8. CPF/CNPJ
    m = _RE_CPF_CNPJ.search(raw)
    if m:
        vat = m.group(1).strip()
        # Search in vat or x_cpf_cnpj field
        domain.append("|")
        domain.append(("vat", "ilike", vat))
        domain.append(("name", "ilike", vat))

    # 9. Amount
    sum_field = aliases.AMOUNT_FIELDS.get(model)
    m = _RE_AMOUNT.search(raw)
    if m and sum_field:
        amount = _parse_amount(m.group(1))
        domain.append((sum_field, ">=", amount))
        title_parts.append(f"> {amount:,.0f}")

    m = _RE_AMOUNT_BELOW.search(raw)
    if m and sum_field:
        amount = _parse_amount(m.group(1))
        domain.append((sum_field, "<=", amount))
        title_parts.append(f"< {amount:,.0f}")

    # 10. Group by (cross-model first, then simple field)
    from cli_anything.odoo_12.core.cross_group import resolve_cross_group

    cross_group_key = None
    group_by_field = None

    # Check if there's a grouping keyword in the query
    has_group_keyword = bool(
        _RE_GROUP_BY.search(raw) or _RE_GROUP_BY_POR.search(raw)
    )

    if has_group_keyword:
        # Try cross-model grouping first (e.g. "por categoria de produto")
        m = _RE_GROUP_BY.search(raw)
        group_text = m.group(1).strip() if m else ""
        if not group_text:
            m = _RE_GROUP_BY_POR.search(raw)
            group_text = m.group(1).strip() if m else ""

        cross_group_key = resolve_cross_group(model, group_text)

        # If no cross-group, try simple field grouping
        if not cross_group_key:
            group_text_lower = group_text.lower()
            for kw in _SORTED_GROUP_KEYWORDS:
                if kw in group_text_lower:
                    group_by_field = _GROUP_BY_KEYWORDS[kw]
                    break

        if not cross_group_key and not group_by_field:
            m = _RE_GROUP_BY_POR.search(raw)
            if m:
                kw = m.group(1).strip().lower()
                # Try cross-group with the keyword
                cross_group_key = resolve_cross_group(model, kw)
                if not cross_group_key:
                    for k in _SORTED_GROUP_KEYWORDS:
                        if k in kw:
                            group_by_field = _GROUP_BY_KEYWORDS[k]
                            break

    if group_by_field and group_by_field not in fields:
        fields = list(fields) + [group_by_field]

    # 11. Compose title
    title = " — ".join(title_parts) if title_parts else model

    return ParsedQuery(
        model=model,
        domain=domain,
        fields=fields,
        limit=limit,
        title=title,
        export=export_intent,
        raw_query=raw,
        sum_field=sum_field,
        group_by=group_by_field,
        cross_group_key=cross_group_key,
    )
