#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""fix_brand_tienda_from_sucursal.py

Llena el campo TIENDA de los contactos de la cuenta de Marca cuando el campo
'Sucursal' (custom field) esta poblado y se puede resolver a una sucursal del
verificador con TIENDA definida.

Es el complemento del bucket "contacts_in_brand_without_tienda" del audit:
para esos contactos NO se puede usar TIENDA como llave (esta vacio), pero
muchos si tienen Sucursal poblada con un valor que mapea claramente a una
sucursal real. Este script copia la TIENDA esperada (segun el verificador)
al custom field TIENDA del contacto en Marca, dejandolo listo para que la
sincronizacion Marca->Sucursal lo pueda procesar.

Reglas estrictas:
  1. Solo procesa contactos cuyo Sucursal resuelve a una sucursal del
     verificador (exact match o substring no ambiguo). Si no resuelve, se
     skipea con razon explicita.
  2. Skip si la sucursal resuelta no tiene TIENDA poblada en el verificador.
  3. NO sobreescribe TIENDA si ya tiene valor (no deberia pasar porque el
     bucket solo trae contactos con TIENDA vacio, pero double-check defensivo).

Modos:
  - dry-run (default): no escribe nada en GHL, solo planea.
  - --apply: ejecuta PUTs en GHL, registra cambios en script_audit y
    actualiza el snapshot local de custom_fields_json.

Uso CLI:
    python scripts/fix_brand_tienda_from_sucursal.py
    python scripts/fix_brand_tienda_from_sucursal.py --apply --yes
    python scripts/fix_brand_tienda_from_sucursal.py --only-contact <id>
    python scripts/fix_brand_tienda_from_sucursal.py --json
"""

import argparse
import json
import os
import sqlite3
import sys
import threading
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime

ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if ROOT_DIR not in sys.path:
    sys.path.insert(0, ROOT_DIR)
SCRIPTS_DIR = os.path.dirname(os.path.abspath(__file__))
if SCRIPTS_DIR not in sys.path:
    sys.path.insert(0, SCRIPTS_DIR)

import script_audit  # noqa: E402
import sync_engine  # noqa: E402
from audit_brand_vs_branches_totals import (  # noqa: E402
    BRAND_LOCATION_ID,
    build_contact_index,
    extract_tienda_from_custom_fields,
    find_match,
    load_accounts_filtered,
    load_contacts,
    load_verifier,
    resolve_tienda_field_id,
    run_audit,
    safe_print,
)

SCRIPT_NAME = os.path.basename(__file__)
from paths import DB_PATH


def _empty_summary():
    return {
        "candidates": 0,
        "contacts_updated": 0,
        "skipped_no_sucursal": 0,
        "skipped_unresolved_sucursal": 0,
        "skipped_already_has_tienda": 0,
        "skipped_ambiguous_physical_match": 0,
        "errors": 0,
    }


def _build_branch_physical_index(conn):
    """Pre-carga contactos de cada sucursal + Ã­ndice de matching para el fallback
    de match fÃ­sico. Devuelve dict: {location_id: {"name": ..., "indices": (by_phone, by_email, by_name)}}.

    Solo considera sucursales no-demo y excluye Marca.
    """
    brand, branches, _demos = load_accounts_filtered(conn)
    index = {}
    for acc in branches:
        loc = acc["location_id"]
        contacts = load_contacts(conn, loc)
        indices = build_contact_index(contacts)
        index[loc] = {
            "name": acc.get("nombre") or loc,
            "indices": indices,
        }
    return index


def _resolve_branch_by_physical_match(brand_contact_row, branch_index):
    """Busca el contacto Marca contra cada sucursal por phone â†’ email â†’ nombre.
    Devuelve (location_id, branch_name, match_kind) si hay UN SOLO match unÃ­voco;
    None si no matchea en ninguna o si matchea en varias (ambiguo).

    El input es un sqlite3.Row o dict del contacto Marca.
    """
    source = {
        "id": brand_contact_row["id"],
        "first_name": brand_contact_row["first_name"],
        "last_name": brand_contact_row["last_name"],
        "phone": brand_contact_row["phone"],
        "email": brand_contact_row["email"],
    }
    hits = []
    for loc, info in branch_index.items():
        by_phone, by_email, by_name = info["indices"]
        matches = find_match(source, by_phone, by_email, by_name)
        if matches:
            # Determinar por quÃ© criterio matcheÃ³ (phone > email > nombre).
            from audit_brand_vs_branches_totals import normalize_phone, normalize_email
            kind = "name"
            sp = normalize_phone(source.get("phone"))
            se = normalize_email(source.get("email"))
            if sp and any(normalize_phone(m.get("phone")) == sp for m in matches):
                kind = "phone"
            elif se and any(normalize_email(m.get("email")) == se for m in matches):
                kind = "email"
            hits.append({"location_id": loc, "branch_name": info["name"], "kind": kind})

    if len(hits) == 1:
        return hits[0]["location_id"], hits[0]["branch_name"], hits[0]["kind"]
    if len(hits) > 1:
        return None  # ambiguo
    return None


def _read_brand_custom_fields(conn, contact_id):
    row = conn.execute(
        "SELECT custom_fields_json FROM contacts WHERE location_id=? AND id=?",
        (BRAND_LOCATION_ID, contact_id),
    ).fetchone()
    if not row:
        return None
    try:
        cfs = json.loads(row["custom_fields_json"] or "[]")
    except Exception:
        cfs = []
    return cfs if isinstance(cfs, list) else []


def _persist_tienda_local(conn, contact_id, tienda_field_id, new_value):
    """Actualiza custom_fields_json en SQLite local para reflejar el cambio."""
    cfs = _read_brand_custom_fields(conn, contact_id) or []
    found = False
    for f in cfs:
        if isinstance(f, dict) and (f.get("id") or f.get("fieldId")) == tienda_field_id:
            f["value"] = new_value
            found = True
            break
    if not found:
        cfs.append({"id": tienda_field_id, "value": new_value})
    conn.execute(
        "UPDATE contacts SET custom_fields_json=? WHERE location_id=? AND id=?",
        (json.dumps(cfs, ensure_ascii=False), BRAND_LOCATION_ID, contact_id),
    )
    conn.commit()


def run_fix(contact_ids=None, dry_run=True, log=None, run_id=None, max_parallel=None):
    """Ejecuta el fill TIENDA <- Sucursal en Marca. Devuelve dict serializable."""
    if log is None:
        log = safe_print
    log_lock = threading.Lock()

    def tlog(msg):
        with log_lock:
            log(msg)

    if not os.path.exists(DB_PATH):
        raise FileNotFoundError(f"No existe {DB_PATH}. Corre una sincronizacion global primero.")

    log(f"[{datetime.now().strftime('%H:%M:%S')}] === fix_brand_tienda_from_sucursal ===")
    log(f"Modo: {'DRY-RUN (no escribe)' if dry_run else 'APPLY (escribe en GHL)'}")

    log("Calculando bucket contacts_in_brand_without_tienda desde audit...")
    audit_data = run_audit(limit_missing=None)
    missing = audit_data["missing"]["contacts_in_brand_without_tienda"]
    targets = missing["items"]
    if contact_ids:
        wanted = set(contact_ids)
        targets = [t for t in targets if t["id"] in wanted]
    log(f"Contactos candidatos: {len(targets)} (total en bucket: {missing['total']})")

    tokens = sync_engine.get_tokens_map()
    brand_token = tokens.get(BRAND_LOCATION_ID)
    if not brand_token:
        raise RuntimeError("No hay token para la cuenta de Marca.")

    conn = sqlite3.connect(DB_PATH)
    conn.row_factory = sqlite3.Row

    summary = _empty_summary()
    summary["candidates"] = len(targets)
    results = []
    updates_planned = []

    try:
        tienda_field_id = resolve_tienda_field_id(conn, BRAND_LOCATION_ID)
        if not tienda_field_id:
            raise RuntimeError(
                "No se pudo resolver el field_id del custom field TIENDA en Marca. "
                "Corre una sincronizacion global para refrescar object_schemas."
            )

        # Pre-cargar Ã­ndice fÃ­sico de sucursales para el fallback: cuando el
        # CF Sucursal no mapea al verificador, intentamos identificar el
        # contacto Marca contra contactos reales de sucursal (phone â†’ email â†’
        # nombre). Si matchea unÃ­vocamente con una sola sucursal, derivamos
        # TIENDA de esa location vÃ­a verificador.
        log("Cargando Ã­ndice fÃ­sico de sucursales (para fallback)...")
        branch_index = _build_branch_physical_index(conn)
        verifier_by_loc, _verifier_by_tienda = load_verifier()
        log(f"Sucursales indexadas: {len(branch_index)} | Verificador: {len(verifier_by_loc)} filas")

        # ---- Fase 1: validacion secuencial ----
        for idx, target in enumerate(targets, 1):
            cid = target["id"]
            display_name = target.get("name") or "(sin nombre)"
            sucursal_value = target.get("sucursal") or ""
            resolved_loc = target.get("sucursal_resolved_location_id")
            resolution_kind = target.get("sucursal_resolution_kind")
            expected_tienda = target.get("expected_tienda")
            expected_branch_name = target.get("expected_branch_name")

            item = {
                "brand_contact_id": cid,
                "name": display_name,
                "phone": target.get("phone"),
                "email": target.get("email"),
                "sucursal": sucursal_value,
                "expected_tienda": expected_tienda,
                "expected_branch_name": expected_branch_name,
                "resolution_kind": resolution_kind,
                "actions": [],
                "status": "pending",
                "error": None,
            }

            try:
                if not sucursal_value:
                    # Sin CF Sucursal poblado: intentar match fÃ­sico contra
                    # sucursales reales antes de skipear. Si el contacto Marca
                    # vive en una sola sucursal, esa es la fuente de verdad.
                    brand_row = conn.execute(
                        "SELECT id, first_name, last_name, phone, email "
                        "FROM contacts WHERE location_id=? AND id=?",
                        (BRAND_LOCATION_ID, cid),
                    ).fetchone()
                    physical = _resolve_branch_by_physical_match(brand_row, branch_index) if brand_row else None
                    if physical:
                        phys_loc, phys_name, phys_kind = physical
                        vinfo = verifier_by_loc.get(phys_loc) or {}
                        phys_tienda = vinfo.get("tienda_raw")
                        if phys_tienda and phys_tienda != "-":
                            resolved_loc = phys_loc
                            expected_tienda = phys_tienda
                            expected_branch_name = phys_name
                            resolution_kind = f"branch_physical_{phys_kind}"
                            item["expected_tienda"] = expected_tienda
                            item["expected_branch_name"] = expected_branch_name
                            item["resolution_kind"] = resolution_kind
                        else:
                            item["status"] = "skipped_no_sucursal"
                            item["actions"].append({
                                "action": "skip_no_sucursal",
                                "physical_match_branch": phys_name,
                                "physical_match_kind": phys_kind,
                                "reason": "match fÃ­sico encontrado pero la sucursal no tiene TIENDA en el verificador",
                            })
                            summary["skipped_no_sucursal"] += 1
                            results.append(item)
                            continue
                    else:
                        item["status"] = "skipped_no_sucursal"
                        item["actions"].append({"action": "skip_no_sucursal"})
                        summary["skipped_no_sucursal"] += 1
                        results.append(item)
                        continue

                if not resolved_loc or not expected_tienda:
                    # Fallback: si el CF Sucursal no mapea al verificador,
                    # intentar identificar al contacto contra sucursales
                    # fÃ­sicas. Si estÃ¡ presente en una sola sucursal, esa
                    # ES su sucursal real â†’ derivar TIENDA de ahÃ­.
                    brand_row = conn.execute(
                        "SELECT id, first_name, last_name, phone, email "
                        "FROM contacts WHERE location_id=? AND id=?",
                        (BRAND_LOCATION_ID, cid),
                    ).fetchone()
                    physical = _resolve_branch_by_physical_match(brand_row, branch_index) if brand_row else None
                    if physical:
                        phys_loc, phys_name, phys_kind = physical
                        vinfo = verifier_by_loc.get(phys_loc) or {}
                        phys_tienda = vinfo.get("tienda_raw")
                        if phys_tienda and phys_tienda != "-":
                            resolved_loc = phys_loc
                            expected_tienda = phys_tienda
                            expected_branch_name = phys_name
                            resolution_kind = f"branch_physical_{phys_kind}"
                            # Actualizar item para que el dashboard refleje la
                            # resoluciÃ³n por match fÃ­sico.
                            item["expected_tienda"] = expected_tienda
                            item["expected_branch_name"] = expected_branch_name
                            item["resolution_kind"] = resolution_kind
                        else:
                            item["status"] = "skipped_unresolved_sucursal"
                            item["actions"].append({
                                "action": "skip_unresolved_sucursal",
                                "sucursal": sucursal_value,
                                "physical_match_branch": phys_name,
                                "physical_match_kind": phys_kind,
                                "reason": "match fÃ­sico encontrado pero la sucursal no tiene TIENDA en el verificador",
                            })
                            summary["skipped_unresolved_sucursal"] += 1
                            results.append(item)
                            continue
                    else:
                        # Sin match fÃ­sico unÃ­voco (ninguno o ambiguo).
                        item["status"] = "skipped_unresolved_sucursal"
                        item["actions"].append({
                            "action": "skip_unresolved_sucursal",
                            "sucursal": sucursal_value,
                        })
                        summary["skipped_unresolved_sucursal"] += 1
                        results.append(item)
                        continue

                # Double-check defensivo: TIENDA actual debe estar vacio.
                cfs = _read_brand_custom_fields(conn, cid) or []
                current_tienda = extract_tienda_from_custom_fields(
                    json.dumps(cfs, ensure_ascii=False), tienda_field_id
                )
                if current_tienda:
                    item["status"] = "skipped_already_has_tienda"
                    item["actions"].append({
                        "action": "skip_already_has_tienda",
                        "current_tienda": current_tienda,
                    })
                    summary["skipped_already_has_tienda"] += 1
                    results.append(item)
                    continue

                payload = {"customFields": [{"id": tienda_field_id, "value": expected_tienda}]}
                item["actions"].append({
                    "action": "update_tienda",
                    "target_location_id": resolved_loc,
                    "target_branch_name": expected_branch_name,
                    "new_tienda": expected_tienda,
                    "resolution_kind": resolution_kind,
                })
                item["status"] = "updated"  # tentativo
                results.append(item)

                if dry_run:
                    summary["contacts_updated"] += 1
                else:
                    updates_planned.append({
                        "item": item,
                        "contact_id": cid,
                        "payload": payload,
                        "expected_tienda": expected_tienda,
                    })
            except Exception as e:
                summary["errors"] += 1
                item["status"] = "error"
                item["error"] = str(e)
                if item not in results:
                    results.append(item)

        # ---- Fase 2: paralelizar PUTs ----
        if not dry_run and updates_planned:
            workers = min(max_parallel or len(updates_planned), len(updates_planned), 20)
            log(f"\nAplicando {len(updates_planned)} updates en paralelo (max_workers={workers})...")

            def _do_update(plan):
                return sync_engine.ghl_client.update_contact(
                    brand_token, plan["contact_id"], plan["payload"]
                )

            t0 = datetime.now()
            with ThreadPoolExecutor(max_workers=workers) as executor:
                future_to_plan = {executor.submit(_do_update, plan): plan for plan in updates_planned}
                for future in as_completed(future_to_plan):
                    plan = future_to_plan[future]
                    item = plan["item"]
                    cid = plan["contact_id"]
                    try:
                        future.result()
                        summary["contacts_updated"] += 1
                        tlog(f"  [OK] {item['name']} -> TIENDA='{plan['expected_tienda']}'")

                        try:
                            _persist_tienda_local(conn, cid, tienda_field_id, plan["expected_tienda"])
                        except Exception as db_exc:
                            tlog(f"  WARN: no se pudo persistir TIENDA local: {db_exc}")

                        if run_id:
                            change_id = script_audit.record_change(
                                run_id, BRAND_LOCATION_ID, "contact", cid,
                                tienda_field_id, "TIENDA",
                                None, plan["expected_tienda"],
                            )
                            if change_id is not None:
                                script_audit.mark_change(change_id, "applied")
                    except Exception as e:
                        summary["errors"] += 1
                        item["status"] = "error"
                        item["error"] = str(e)
                        tlog(f"  [ERROR] {item['name']}: {e}")
            elapsed = (datetime.now() - t0).total_seconds()
            log(f"Updates paralelos terminados en {elapsed:.1f}s ({len(updates_planned)} requests).")

        log("\n=== RESUMEN ===")
        log(f"  Candidatos                : {summary['candidates']}")
        log(f"  Contactos {'a actualizar' if dry_run else 'actualizados'} : {summary['contacts_updated']}")
        log(f"  Skip sin Sucursal         : {summary['skipped_no_sucursal']}")
        log(f"  Skip Sucursal sin match   : {summary['skipped_unresolved_sucursal']}")
        log(f"  Skip TIENDA ya poblada    : {summary['skipped_already_has_tienda']}")
        log(f"  Errores                   : {summary['errors']}")

        return {
            "dry_run": dry_run,
            "summary": summary,
            "items": results,
        }
    finally:
        conn.close()


def main():
    parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    parser.add_argument("--apply", action="store_true", help="Ejecuta las escrituras en GHL. Default dry-run.")
    parser.add_argument("--yes", action="store_true", help="Skip confirmacion interactiva.")
    parser.add_argument("--only-contact", action="append", default=[], help="Procesa solo el contact_id dado (repetible).")
    parser.add_argument("--run-id", type=str, default=None, help="Id de script_audit. Solo aplica con --apply.")
    parser.add_argument("--json", action="store_true", help="Imprime el resultado como JSON.")
    args = parser.parse_args()

    dry_run = not args.apply
    if not dry_run and not args.yes:
        confirm = input("Esto escribira en GHL. Continuar? (y/N): ").strip().lower()
        if confirm not in ("y", "yes", "s", "si", "sÃ­"):
            print("Cancelado.")
            sys.exit(0)

    try:
        result = run_fix(
            contact_ids=args.only_contact or None,
            dry_run=dry_run,
            log=safe_print,
            run_id=args.run_id,
        )
    except FileNotFoundError as e:
        safe_print(f"ERROR: {e}")
        sys.exit(2)
    except RuntimeError as e:
        safe_print(f"ERROR: {e}")
        sys.exit(3)

    if args.json:
        safe_print(json.dumps(result, ensure_ascii=False, indent=2, default=str))


if __name__ == "__main__":
    main()
