#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Copia el ID NATIVO de cada contacto a su propio custom field
"ID Contacto Sucursal" (fieldKey: contact.id_contacto_sucursal).

SOLO aplica a SUCURSALES (excluye la cuenta de Marca). En la sucursal, cada
contacto queda autoetiquetado con su propio `id`. Propósito: habilitar la
sincronización determinística sucursal↔Marca por este campo (análogo al de
oportunidades). El workflow n8n y el sync_missing_contacts usan este valor
para encontrar/actualizar el contacto Marca correspondiente sin heurísticas.

Flujo:
  - Lee TODOS los contactos de cada sucursal EN VIVO (paginado), no del cache,
    para tener el estado actual del custom field.
  - Para cada contacto, si el campo ya vale exactamente su `id`, se omite (idempotente).
  - Si no, PUT /contacts/{id} con customFields=[{id,key,field_value: contact.id}].
    GHL MERGEA customFields en el PUT (validado): preserva los demás.

Cada cambio se registra en script_audit (rollback) y se guarda un snapshot por
cuenta en generated/migrations/. Sin --apply corre en DRY-RUN.

Uso:
    python scripts/fill_contact_id_sucursal.py --location <id>           # dry-run
    python scripts/fill_contact_id_sucursal.py --demos --apply --run-id <uuid>
    python scripts/fill_contact_id_sucursal.py --all --apply --run-id <uuid>
"""

import argparse
import datetime
import json
import os
import sys
import warnings

warnings.filterwarnings("ignore", message=r"urllib3 .* doesn't match a supported version!")

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)

import sync_engine  # noqa: E402
import script_audit  # noqa: E402
from paths import MIGRATIONS_DIR  # noqa: E402

BRAND_LOCATION_ID = "GbKkBpCmKu2QmloKFHy3"
DEMO_LOCATION_IDS = ["Vf7qQl3L9vakJ8hDtQ8e", "Z64WQKORPVwXb5mn68Ef"]
FIELD_FKEY = "contact.id_contacto_sucursal"
FIELD_NAME_NORM = "id contacto sucursal"

gc = sync_engine.ghl_client


def safe_print(*args, **kwargs):
    text = " ".join(str(a) for a in args)
    try:
        sys.stdout.write(text + "\n")
        sys.stdout.flush()
    except UnicodeEncodeError:
        enc = sys.stdout.encoding or "utf-8"
        sys.stdout.write(text.encode(enc, errors="replace").decode(enc) + "\n")
        sys.stdout.flush()


def norm(s):
    return " ".join(str(s or "").strip().lower().split())


def resolve_field(location_id, token):
    """(field_id, field_key) del campo en una location (legacy endpoint)."""
    data = gc._request("GET", f"/locations/{location_id}/customFields", token,
                       params={"model": "contact"})
    for cf in data.get("customFields", []) or []:
        if cf.get("fieldKey") == FIELD_FKEY or norm(cf.get("name")) == FIELD_NAME_NORM:
            return cf.get("id"), cf.get("fieldKey")
    return None, None


def search_all_contacts(token, location_id, page_limit=100):
    """Lista todos los contactos de una location vía POST /contacts/search, que SÍ
    incluye customFields en cada item (a diferencia de GET /contacts/). Pagina por
    `searchAfter` que viene en el último contact de cada batch.

    Defensa anti-loop: tope de páginas + corta si batch < page_limit.
    """
    out = []
    search_after = None
    page_count = 0
    while page_count < 500:  # tope defensivo (= 500 * 100 = 50k contactos)
        body = {"locationId": location_id, "pageLimit": page_limit}
        if search_after is not None:
            body["searchAfter"] = search_after
        data = gc._request("POST", "/contacts/search", token, json=body)
        batch = data.get("contacts") or []
        if not batch:
            break
        out.extend(batch)
        page_count += 1
        if len(batch) < page_limit:
            break
        last = batch[-1]
        new_after = last.get("searchAfter")
        if not new_after or new_after == search_after:
            break
        search_after = new_after
    return out


def current_field_value(contact, field_id):
    """Valor actual del CF en un contacto del search (maneja variantes GHL)."""
    for cf in contact.get("customFields") or []:
        if cf.get("id") == field_id or cf.get("fieldId") == field_id:
            return cf.get("value") or cf.get("fieldValue") or cf.get("fieldValueString")
    return None


def plan_account(account):
    loc = account["location_id"]
    token = account["token"]
    field_id, field_key = resolve_field(loc, token)
    if not field_id:
        return None, None, [{"status": "field_missing",
                             "details": "El campo 'ID Contacto Sucursal' no existe en esta cuenta."}]
    contacts = search_all_contacts(token, loc)
    actions = []
    for c in contacts:
        cid = c.get("id")
        if not cid:
            continue
        cur = current_field_value(c, field_id)
        if cur == cid:
            actions.append({"status": "already_ok", "contact_id": cid})
        else:
            name = f"{c.get('firstName') or ''} {c.get('lastName') or ''}".strip() or c.get("contactName") or ""
            actions.append({"status": "to_set", "contact_id": cid,
                            "old": cur, "new": cid, "name": name})
    return field_id, field_key, actions


def apply_actions(account, field_id, field_key, actions, *, dry_run, run_id):
    stats = {"set": 0, "skipped": 0, "errors": []}
    to_set = [a for a in actions if a["status"] == "to_set"]
    snap = {
        "account": account["nombre"], "location_id": account["location_id"],
        "field_id": field_id, "field_key": field_key,
        "timestamp_utc": datetime.datetime.now(datetime.timezone.utc).isoformat(),
        "dry_run": dry_run,
        "total_contacts": len(actions),
        "to_set": [{"contact_id": a["contact_id"], "old": a.get("old"), "new": a["new"]} for a in to_set],
    }
    os.makedirs(MIGRATIONS_DIR, exist_ok=True)
    ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
    safe = "".join(c if c.isalnum() else "_" for c in account["nombre"])[:40]
    snap_path = os.path.join(MIGRATIONS_DIR, f"fill_contactid_{safe}_{ts}.json")
    with open(snap_path, "w", encoding="utf-8") as fh:
        json.dump(snap, fh, ensure_ascii=False, indent=2, default=str)

    stats["skipped"] = sum(1 for a in actions if a["status"] != "to_set")
    if dry_run:
        return stats, snap_path

    token = account["token"]
    for a in to_set:
        cid = a["contact_id"]
        change_id = None
        if run_id:
            change_id = script_audit.record_change(
                run_id, account["location_id"], "contact",
                cid, field_id, FIELD_NAME_NORM, {"value": a.get("old")}, {"value": cid})
        try:
            gc._request("PUT", f"/contacts/{cid}", token, json={
                "customFields": [{"id": field_id, "key": field_key, "field_value": cid}]
            })
            if change_id:
                script_audit.mark_change(change_id, "applied")
            stats["set"] += 1
        except Exception as exc:
            if change_id:
                script_audit.mark_change(change_id, "failed", str(exc))
            stats["errors"].append({"contact_id": cid, "error": str(exc)})
    return stats, snap_path


def print_plan(account, actions):
    to_set = [a for a in actions if a["status"] == "to_set"]
    ok = sum(1 for a in actions if a["status"] == "already_ok")
    miss = [a for a in actions if a["status"] == "field_missing"]
    safe_print(f"\n[Plan] {account['nombre']} ({account['location_id']})")
    if miss:
        safe_print(f"  ✗ {miss[0]['details']}")
        return
    safe_print(f"  contactos totales={len(actions)}  ya ok={ok}  a setear={len(to_set)}")
    for a in to_set[:6]:
        safe_print(f"  ▸ contact {a['contact_id']}  {a.get('name')!r}  campo: {a.get('old')!r} → {a['new']!r}")
    if len(to_set) > 6:
        safe_print(f"  … y {len(to_set) - 6} más")


def select_targets(args, accounts):
    if args.location:
        m = [a for a in accounts if a["location_id"] == args.location]
        if not m:
            raise SystemExit(f"Location {args.location} no encontrada.")
        if args.location == BRAND_LOCATION_ID:
            raise SystemExit("Este script es SOLO para sucursales; no aplica a Marca.")
        return m
    if args.demos:
        return [a for a in accounts if a["location_id"] in DEMO_LOCATION_IDS]
    if args.all_productive:
        return [a for a in accounts
                if a["location_id"] not in DEMO_LOCATION_IDS
                and a["location_id"] != BRAND_LOCATION_ID]
    if args.all:
        return [a for a in accounts if a["location_id"] != BRAND_LOCATION_ID]
    raise SystemExit("Especifica --location <id>, --demos, --all-productive o --all.")


def run_fill(location_ids=None, contact_ids=None, dry_run=True, run_id=None, log=None):
    """Ejecuta el llenado del campo 'ID Contacto Sucursal' y devuelve un dict
    JSON-serializable. Pensado para uso programático (endpoint del dashboard) y CLI.

    Args:
        location_ids: lista de location_ids (sucursales) a procesar. None = todas
            las sucursales (excluye Marca). NUNCA toca Marca (defensa).
        contact_ids: iterable opcional de contact_ids para filtrar (solo se
            actualizan los contactos en esta lista). None = todos los detectados.
        dry_run: True (default) = no escribe en GHL.
        run_id: id para script_audit (rollback).
        log: callable opcional log(line).
    """
    if log is None:
        log = safe_print

    contact_filter = set(contact_ids) if contact_ids else None

    accounts = sync_engine.parse_accounts_csv()
    accounts_by_id = {a["location_id"]: a for a in accounts}

    if location_ids:
        targets = []
        for lid in location_ids:
            if lid == BRAND_LOCATION_ID:
                continue
            acc = accounts_by_id.get(lid)
            if acc:
                targets.append(acc)
    else:
        targets = [a for a in accounts if a["location_id"] != BRAND_LOCATION_ID]

    if run_id:
        script_audit.create_run(
            run_id, "fill_contact_id_sucursal.py",
            arguments=("contact_ids:%d" % len(contact_filter)) if contact_filter else "all-branches",
            locations=[t["location_id"] for t in targets])

    log(f"Modo: {'DRY-RUN' if dry_run else 'APPLY'}")
    log(f"Cuentas en scope (sucursales): {len(targets)}")
    if contact_filter is not None:
        log(f"Filtro contact_ids: {len(contact_filter)} contactos")

    summary = {
        "accounts_processed": 0,
        "contacts_reviewed": 0,
        "set": 0,
        "skipped": 0,
        "errors": 0,
    }
    items = []
    errors_global = []

    for acc in targets:
        if not script_audit.wait_if_paused_or_stopped(run_id):
            log("Detención solicitada. Saliendo.")
            break
        item = {"location_id": acc["location_id"], "account": acc["nombre"]}
        try:
            field_id, field_key, actions = plan_account(acc)
        except Exception as exc:
            log(f"  [{acc['nombre']}] ERROR plan: {exc}")
            errors_global.append({"account": acc["nombre"], "error": str(exc)})
            summary["errors"] += 1
            item.update({"status": "plan_error", "error": str(exc)})
            items.append(item)
            continue
        if contact_filter is not None:
            actions = [a for a in actions if a.get("contact_id") in contact_filter]
        contacts_in_scope = sum(1 for a in actions if a.get("status") in ("to_set", "already_ok"))
        summary["contacts_reviewed"] += contacts_in_scope
        print_plan(acc, actions)
        if field_id is None:
            item.update({"status": "field_missing", "contacts_in_scope": contacts_in_scope})
            items.append(item)
            continue
        try:
            stats, snap_path = apply_actions(acc, field_id, field_key, actions,
                                             dry_run=dry_run, run_id=run_id)
            summary["set"] += stats["set"]
            summary["skipped"] += stats["skipped"]
            errors_global.extend(stats["errors"])
            summary["errors"] += len(stats["errors"])
            summary["accounts_processed"] += 1
            log(f"  snapshot: {snap_path}")
            if not dry_run:
                log(f"  ✓ set={stats['set']}  skipped={stats['skipped']}  errors={len(stats['errors'])}")
            item.update({
                "status": "ok",
                "contacts_in_scope": contacts_in_scope,
                "set": stats["set"],
                "skipped": stats["skipped"],
                "errors": len(stats["errors"]),
                "snapshot": snap_path,
            })
            items.append(item)
        except Exception as exc:
            log(f"  ✗ apply falló: {exc}")
            errors_global.append({"account": acc["nombre"], "error": str(exc)})
            summary["errors"] += 1
            item.update({"status": "apply_error", "error": str(exc)})
            items.append(item)

    log("\n" + "=" * 60)
    if dry_run:
        log("DRY-RUN — no se modificó nada. Revisa el plan y vuelve a correr con --apply.")
    log(f"Cuentas procesadas : {len(targets)}")
    log(f"Contactos revisados: {summary['contacts_reviewed']}")
    log(f"Campos seteados    : {summary['set']}")
    log(f"Saltados (ya ok)   : {summary['skipped']}")
    log(f"Errores            : {len(errors_global)}")
    for e in errors_global[:20]:
        log(f"   - {e}")

    return {
        "dry_run": dry_run,
        "summary": summary,
        "items": items,
        "errors": errors_global,
    }


def main():
    parser = argparse.ArgumentParser(
        description="Copia el id nativo de cada contacto a su custom field 'ID Contacto Sucursal' (solo sucursales).")
    parser.add_argument("--location", help="Procesar solo esta cuenta (no Marca).")
    parser.add_argument("--demos", action="store_true", help="Solo las 2 cuentas DEMO.")
    parser.add_argument("--all-productive", dest="all_productive", action="store_true",
                        help="Todas las sucursales productivas (excluye Marca y DEMOs).")
    parser.add_argument("--all", action="store_true", help="Todas las sucursales (excluye Marca).")
    parser.add_argument("--apply", action="store_true",
                        help="Ejecuta los cambios. Sin este flag corre en DRY-RUN.")
    parser.add_argument("--run-id", help="ID para registrar en script_audit (rollback).")
    args = parser.parse_args()

    if hasattr(sys.stdout, "reconfigure"):
        sys.stdout.reconfigure(encoding="utf-8")

    accounts = sync_engine.parse_accounts_csv()
    targets = select_targets(args, accounts)
    location_ids = [t["location_id"] for t in targets]
    run_fill(location_ids=location_ids, contact_ids=None,
             dry_run=not args.apply, run_id=args.run_id, log=safe_print)


if __name__ == "__main__":
    main()
