from __future__ import annotations

import json
import re
from typing import Any, Dict, List, Optional, Tuple

from sqlalchemy import text

from app.db import engine
from app.commercial_service import ensure_commercial_schema, normalize_tax_id
from app.opportunity_service import ensure_opportunity_schema


def _clean(value: Any, max_len: int = 260) -> str:
    if value is None:
        return ""
    text_value = re.sub(r"\s+", " ", str(value).replace("\n", " ")).strip()
    return text_value[:max_len]


def _json(value: Any) -> str:
    return json.dumps(value or {}, ensure_ascii=False, default=str)


def _as_dict(value: Any) -> Dict[str, Any]:
    return value if isinstance(value, dict) else {}


def _first_from(data: Dict[str, Any], *keys: str) -> str:
    for key in keys:
        if key in data and _clean(data.get(key)):
            return _clean(data.get(key))
    return ""


def _deep_first(data: Dict[str, Any], *keys: str) -> str:
    stack: List[Any] = [data]
    seen = 0
    while stack and seen < 100:
        seen += 1
        current = stack.pop(0)
        if isinstance(current, dict):
            direct = _first_from(current, *keys)
            if direct:
                return direct
            for value in current.values():
                if isinstance(value, (dict, list)):
                    stack.append(value)
        elif isinstance(current, list):
            stack.extend(v for v in current if isinstance(v, (dict, list)))
    return ""


def _split_pt_address(address: str) -> Tuple[str, str, str]:
    clean = _clean(address, 400)
    if not clean:
        return "", "", ""
    match = re.search(r"\b(\d{4}-\d{3}|\d{4})\b\s*(.*)$", clean)
    if not match:
        return clean, "", ""
    return clean[: match.start()].strip(" ,-"), match.group(1), _clean(match.group(2), 90)


def _normalize_customer_payload(doc: Dict[str, Any]) -> Dict[str, Any]:
    payload = _as_dict(doc.get("payload"))
    record = _as_dict(payload.get("record") or payload.get("jasmin_details") or payload.get("jasmin_payload") or payload)
    nested_customer: Dict[str, Any] = {}
    for key in ("customer", "customerParty", "buyerCustomerParty", "party", "billing", "invoiceTo", "shipTo"):
        if isinstance(record.get(key), dict):
            nested_customer.update(record.get(key) or {})
    merged = {**record, **nested_customer}

    name = _deep_first(merged, "customerName", "partyName", "buyerCustomerPartyName", "name", "companyName") or _clean(doc.get("company"))
    tax_id = normalize_tax_id(_deep_first(merged, "customerTaxId", "customerTaxID", "companyTaxID", "companyTaxId", "partyTaxID", "taxID", "taxId", "vat", "nif"))
    email = _deep_first(merged, "customerEmail", "electronicMail", "email", "billingEmail")
    phone = _deep_first(merged, "phone", "telephone", "mobile", "phoneNumber")
    street = _deep_first(merged, "streetName", "street", "address", "billingAddress", "addressLine1", "line1")
    parsed_street, parsed_zip, parsed_city = _split_pt_address(street)
    street = parsed_street or street
    postal = _deep_first(merged, "postalZone", "postalCode", "zip", "postcode") or parsed_zip
    city = _deep_first(merged, "cityName", "city", "locality") or parsed_city
    country = _deep_first(merged, "country", "countryCode", "countryKey") or "PT"
    party_key = _deep_first(merged, "customerPartyKey", "buyerCustomerParty", "partyKey", "naturalKey") or _clean(doc.get("customer_party_key"))

    return {k: v for k, v in {
        "name": name,
        "tax_id": tax_id,
        "email": email,
        "phone": phone,
        "street_name": street,
        "postal_zone": postal,
        "city_name": city,
        "country": country or "PT",
        "jasmin_customer_party_key": party_key,
        "jasmin_customer_id": _deep_first(merged, "customerId", "id", "uuid"),
    }.items() if _clean(v)}


def _missing_fields(customer: Dict[str, Any]) -> List[str]:
    labels = []
    for key, label in [
        ("email", "email de faturação"),
        ("street_name", "morada fiscal"),
        ("postal_zone", "código postal"),
        ("city_name", "localidade"),
    ]:
        if not _clean(customer.get(key)):
            labels.append(label)
    return labels


def _select_candidate_doc(conn: Any, opportunity_id: str) -> Optional[Dict[str, Any]]:
    rows = conn.execute(text("""
        SELECT
            d.id::text,
            d.customer_id::text,
            d.document_kind,
            d.document_number,
            d.customer_party_key,
            d.payload,
            d.company,
            d.created_at,
            d.updated_at,
            c.id::text AS doc_customer_id,
            c.name AS doc_customer_name,
            c.tax_id AS doc_customer_tax_id,
            c.email AS doc_customer_email,
            c.phone AS doc_customer_phone,
            c.street_name AS doc_customer_street_name,
            c.postal_zone AS doc_customer_postal_zone,
            c.city_name AS doc_customer_city_name,
            c.country AS doc_customer_country,
            c.jasmin_customer_party_key AS doc_customer_party_key,
            c.jasmin_customer_id AS doc_customer_jasmin_id
        FROM commercial_documents d
        LEFT JOIN customers c ON c.id = d.customer_id
        WHERE d.opportunity_id = CAST(:opportunity_id AS UUID)
          AND d.system = 'jasmin'
          AND d.document_kind IN ('quotation', 'invoice', 'proforma')
          AND COALESCE(d.is_active, TRUE) = TRUE
        ORDER BY
          CASE d.document_kind WHEN 'invoice' THEN 1 WHEN 'quotation' THEN 2 WHEN 'proforma' THEN 3 ELSE 4 END,
          COALESCE(d.document_date, d.created_at::date) DESC,
          d.created_at DESC
        LIMIT 5
    """), {"opportunity_id": opportunity_id}).mappings().all()
    best = None
    best_score = -1
    for row in rows:
        doc = dict(row)
        candidate = _normalize_customer_payload(doc)
        if doc.get("doc_customer_id"):
            candidate = {
                **candidate,
                "id": doc.get("doc_customer_id"),
                "name": candidate.get("name") or doc.get("doc_customer_name"),
                "tax_id": candidate.get("tax_id") or doc.get("doc_customer_tax_id"),
                "email": candidate.get("email") or doc.get("doc_customer_email"),
                "phone": candidate.get("phone") or doc.get("doc_customer_phone"),
                "street_name": candidate.get("street_name") or doc.get("doc_customer_street_name"),
                "postal_zone": candidate.get("postal_zone") or doc.get("doc_customer_postal_zone"),
                "city_name": candidate.get("city_name") or doc.get("doc_customer_city_name"),
                "country": candidate.get("country") or doc.get("doc_customer_country") or "PT",
                "jasmin_customer_party_key": candidate.get("jasmin_customer_party_key") or doc.get("doc_customer_party_key"),
                "jasmin_customer_id": candidate.get("jasmin_customer_id") or doc.get("doc_customer_jasmin_id"),
            }
        score = sum(1 for key in ("name", "tax_id", "email", "street_name", "postal_zone", "city_name") if _clean(candidate.get(key)))
        if score > best_score:
            best_score = score
            best = {"document": doc, "candidate": candidate}
    return best


def get_jasmin_fiscal_sync_preview(opportunity_id: str) -> Dict[str, Any]:
    """Read-only preview for fiscal data available in linked Jasmin documents."""
    ensure_opportunity_schema()
    ensure_commercial_schema()
    with engine.begin() as conn:
        opp = conn.execute(text("""
            SELECT o.id::text, o.local_customer_id::text, o.title,
                   c.id::text AS customer_id, c.name, c.tax_id, c.email, c.phone,
                   c.street_name, c.postal_zone, c.city_name, c.country,
                   c.jasmin_customer_party_key, c.jasmin_customer_id
            FROM opportunities o
            LEFT JOIN customers c ON c.id = o.local_customer_id
            WHERE o.id = CAST(:opportunity_id AS UUID)
            LIMIT 1
        """), {"opportunity_id": opportunity_id}).mappings().first()
        if not opp:
            return {"available": False, "reason": "opportunity_not_found"}
        selected = _select_candidate_doc(conn, opportunity_id)
        if not selected:
            return {"available": False, "reason": "no_jasmin_document"}
        candidate = selected["candidate"]
        if not candidate.get("id") and candidate.get("tax_id"):
            row = conn.execute(text("""
                SELECT id::text, name, tax_id, email, phone, street_name, postal_zone,
                       city_name, country, jasmin_customer_party_key, jasmin_customer_id
                FROM customers
                WHERE tax_id = :tax_id
                LIMIT 1
            """), {"tax_id": candidate.get("tax_id")}).mappings().first()
            if row:
                c = dict(row)
                candidate = {**candidate, "id": c.get("id")}
                for key in ("name", "tax_id", "email", "phone", "street_name", "postal_zone", "city_name", "country", "jasmin_customer_party_key", "jasmin_customer_id"):
                    candidate[key] = candidate.get(key) or c.get(key)
        linked = dict(opp)
        linked_tax = normalize_tax_id(linked.get("tax_id"))
        cand_tax = normalize_tax_id(candidate.get("tax_id"))
        conflict = bool(linked.get("customer_id") and linked_tax and cand_tax and linked_tax != cand_tax)
        missing_now = _missing_fields(linked if linked.get("customer_id") else {})
        fillable = [key for key in ("email", "phone", "street_name", "postal_zone", "city_name", "country") if not _clean(linked.get(key)) and _clean(candidate.get(key))]
        if not linked.get("customer_id"):
            fillable = [key for key in ("name", "tax_id", "email", "phone", "street_name", "postal_zone", "city_name", "country") if _clean(candidate.get(key))]
    return {
        "available": bool(candidate.get("name") or candidate.get("tax_id")),
        "conflict": conflict,
        "linked_customer_id": linked.get("customer_id"),
        "candidate_customer_id": candidate.get("id"),
        "candidate": candidate,
        "document": {k: selected["document"].get(k) for k in ("id", "document_kind", "document_number")},
        "fillable_fields": fillable,
        "missing_fields": missing_now,
        "reason": "nif_divergent" if conflict else "ok",
    }


def apply_jasmin_fiscal_sync(opportunity_id: str, *, actor: str = "operator") -> Dict[str, Any]:
    """Fill only empty customer fields from Jasmin and link/create when safe."""
    ensure_opportunity_schema()
    ensure_commercial_schema()
    preview = get_jasmin_fiscal_sync_preview(opportunity_id)
    if not preview.get("available"):
        raise ValueError("Não foram encontrados dados fiscais Jasmin associados a esta oportunidade.")
    if preview.get("conflict"):
        raise ValueError("NIF divergente entre cliente fiscal local e dados Jasmin; rever associação antes de importar.")
    candidate = dict(preview.get("candidate") or {})
    if not (candidate.get("name") or candidate.get("tax_id")):
        raise ValueError("Dados Jasmin insuficientes para criar/associar cliente fiscal.")

    with engine.begin() as conn:
        opp = conn.execute(text("""
            SELECT o.id::text, o.local_customer_id::text
            FROM opportunities o
            WHERE o.id = CAST(:opportunity_id AS UUID)
            LIMIT 1
        """), {"opportunity_id": opportunity_id}).mappings().first()
        linked_customer_id = str(opp.get("local_customer_id") or "") if opp else ""
        target_customer_id = linked_customer_id or str(preview.get("candidate_customer_id") or "")
        created = False
        if target_customer_id:
            existing = conn.execute(text("SELECT id::text FROM customers WHERE id = CAST(:id AS UUID) LIMIT 1"), {"id": target_customer_id}).mappings().first()
            if not existing:
                target_customer_id = ""
        if not target_customer_id:
            target_customer_id = conn.execute(text("""
                INSERT INTO customers (
                    name, tax_id, email, phone, street_name, postal_zone, city_name, country,
                    jasmin_customer_party_key, jasmin_customer_id, metadata, updated_at
                ) VALUES (
                    :name, NULLIF(:tax_id,''), NULLIF(:email,''), NULLIF(:phone,''), NULLIF(:street_name,''),
                    NULLIF(:postal_zone,''), NULLIF(:city_name,''), COALESCE(NULLIF(:country,''), 'PT'),
                    NULLIF(:party_key,''), NULLIF(:jasmin_id,''), CAST(:metadata AS JSONB), now()
                )
                RETURNING id::text
            """), {
                "name": _clean(candidate.get("name")) or "Cliente Jasmin",
                "tax_id": normalize_tax_id(candidate.get("tax_id")),
                "email": _clean(candidate.get("email")),
                "phone": _clean(candidate.get("phone")),
                "street_name": _clean(candidate.get("street_name")),
                "postal_zone": _clean(candidate.get("postal_zone")),
                "city_name": _clean(candidate.get("city_name")),
                "country": _clean(candidate.get("country") or "PT"),
                "party_key": _clean(candidate.get("jasmin_customer_party_key")),
                "jasmin_id": _clean(candidate.get("jasmin_customer_id")),
                "metadata": _json({"source": "jasmin_fiscal_sync", "opportunity_id": opportunity_id}),
            }).scalar()
            created = True
        before = conn.execute(text("""
            SELECT id::text, name, tax_id, email, phone, street_name, postal_zone,
                   city_name, country, jasmin_customer_party_key, jasmin_customer_id
            FROM customers WHERE id = CAST(:id AS UUID)
        """), {"id": target_customer_id}).mappings().first()
        before_dict = dict(before or {})
        local_tax = normalize_tax_id(before_dict.get("tax_id"))
        jasmin_tax = normalize_tax_id(candidate.get("tax_id"))
        if local_tax and jasmin_tax and local_tax != jasmin_tax:
            raise ValueError("NIF divergente entre cliente local e Jasmin; importação bloqueada.")
        params = {
            "id": target_customer_id,
            "tax_id": jasmin_tax,
            "email": _clean(candidate.get("email")),
            "phone": _clean(candidate.get("phone")),
            "street_name": _clean(candidate.get("street_name")),
            "postal_zone": _clean(candidate.get("postal_zone")),
            "city_name": _clean(candidate.get("city_name")),
            "country": _clean(candidate.get("country") or "PT"),
            "party_key": _clean(candidate.get("jasmin_customer_party_key")),
            "jasmin_id": _clean(candidate.get("jasmin_customer_id")),
            "metadata": _json({"jasmin_fiscal_sync": {"opportunity_id": opportunity_id, "document": preview.get("document"), "actor": actor}}),
        }
        row = conn.execute(text("""
            UPDATE customers
            SET tax_id = COALESCE(NULLIF(tax_id,''), NULLIF(:tax_id,'')),
                email = COALESCE(NULLIF(email,''), NULLIF(:email,'')),
                phone = COALESCE(NULLIF(phone,''), NULLIF(:phone,'')),
                street_name = COALESCE(NULLIF(street_name,''), NULLIF(:street_name,'')),
                postal_zone = COALESCE(NULLIF(postal_zone,''), NULLIF(:postal_zone,'')),
                city_name = COALESCE(NULLIF(city_name,''), NULLIF(:city_name,'')),
                country = COALESCE(NULLIF(country,''), NULLIF(:country,''), 'PT'),
                jasmin_customer_party_key = COALESCE(NULLIF(jasmin_customer_party_key,''), NULLIF(:party_key,'')),
                jasmin_customer_id = COALESCE(NULLIF(jasmin_customer_id,''), NULLIF(:jasmin_id,'')),
                metadata = COALESCE(metadata, '{}'::jsonb) || CAST(:metadata AS JSONB),
                updated_at = now()
            WHERE id = CAST(:id AS UUID)
            RETURNING id::text, name, tax_id, email, phone, street_name, postal_zone,
                      city_name, country, jasmin_customer_party_key, jasmin_customer_id
        """), params).mappings().first()
        conn.execute(text("""
            UPDATE opportunities
            SET local_customer_id = CAST(:customer_id AS UUID), updated_at = now()
            WHERE id = CAST(:opportunity_id AS UUID)
              AND (local_customer_id IS NULL OR local_customer_id = CAST(:customer_id AS UUID))
        """), {"customer_id": target_customer_id, "opportunity_id": opportunity_id})
        conn.execute(text("""
            UPDATE commercial_documents
            SET customer_id = COALESCE(customer_id, CAST(:customer_id AS UUID)), updated_at = now()
            WHERE opportunity_id = CAST(:opportunity_id AS UUID)
              AND system = 'jasmin'
        """), {"customer_id": target_customer_id, "opportunity_id": opportunity_id})
        after_dict = dict(row or {})
        filled = [key for key in ("tax_id", "email", "phone", "street_name", "postal_zone", "city_name", "country") if not _clean(before_dict.get(key)) and _clean(after_dict.get(key))]
        conn.execute(text("""
            INSERT INTO opportunity_events (id, opportunity_id, event_type, note, payload, created_by)
            VALUES (gen_random_uuid(), CAST(:opportunity_id AS UUID), 'jasmin_fiscal_sync', :note, CAST(:payload AS JSONB), :actor)
        """), {
            "opportunity_id": opportunity_id,
            "note": "Dados fiscais completados a partir do Jasmin." if filled or created else "Cliente fiscal associado a partir do Jasmin.",
            "payload": _json({"customer_id": target_customer_id, "created_customer": created, "filled_fields": filled, "document": preview.get("document"), "candidate": candidate}),
            "actor": actor,
        })
    return {"customer_id": target_customer_id, "created_customer": created, "filled_fields": filled, "customer": after_dict}


def audit_jasmin_fiscal_gaps(limit: int = 200) -> List[Dict[str, Any]]:
    ensure_opportunity_schema()
    ensure_commercial_schema()
    findings: List[Dict[str, Any]] = []
    with engine.begin() as conn:
        rows = conn.execute(text("""
            SELECT DISTINCT o.id::text, o.title, o.customer_name
            FROM opportunities o
            JOIN commercial_documents d ON d.opportunity_id = o.id AND d.system = 'jasmin'
            LEFT JOIN customers c ON c.id = o.local_customer_id
            WHERE COALESCE(o.status, 'open') <> 'closed'
              AND (
                o.local_customer_id IS NULL
                OR COALESCE(c.email,'') = ''
                OR COALESCE(c.street_name,'') = ''
                OR COALESCE(c.postal_zone,'') = ''
                OR COALESCE(c.city_name,'') = ''
              )
            ORDER BY o.id
            LIMIT :limit
        """), {"limit": int(limit)}).mappings().all()
    for row in rows:
        preview = get_jasmin_fiscal_sync_preview(str(row.get("id")))
        if preview.get("available") and (preview.get("fillable_fields") or not preview.get("linked_customer_id")):
            cand = preview.get("candidate") or {}
            findings.append({
                "severity": "médio" if not preview.get("linked_customer_id") else "baixo",
                "code": "jasmin_fiscal_data_available",
                "title": row.get("title") or row.get("customer_name") or row.get("id"),
                "detail": "Dados fiscais vazios no ClientFlow mas disponíveis no Jasmin.",
                "url": f"/opportunities/{row.get('id')}",
                "candidate_name": cand.get("name"),
                "candidate_tax_id": cand.get("tax_id"),
            })
    return findings
