#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""create_missing_branch_opps_in_marca.py

Crea en la cuenta de Marca (GbKkBpCmKu2QmloKFHy3) las oportunidades de sucursal
que NO estan replicadas, manejando correctamente el caso MULTI-EMPENO (un contacto
con 2+ opps en sucursal donde Marca solo tiene la 1a).

POR QUE UN SCRIPT NUEVO (vs sync_missing_opps_to_brand.py):
  - El bucket del audit (opportunities_in_branch_not_in_brand) DESCARTA una opp de
    sucursal no-replicada si su contacto ya tiene CUALQUIER opp en Marca
    (audit_brand_vs_branches_totals.py:1090-1092 `has_brand_opp`). Eso oculta justo
    las extras de multi-empeno.
  - `_pick_existing` de sync_missing_opps_to_brand.py (lineas 838-854) cae a
    "primera open / primera" y ACTUALIZARIA la 1a opp del contacto en vez de crear
    la 2a.

Aqui la identificacion es ESTRICTA por link: una opp de sucursal esta "faltante"
solo si su id nativo NO aparece como valor del CF "ID Oportunidad Sucursal"
(opportunity.id_oportunidad_sucursal) en NINGUNA opp de Marca. La decision
create/no-create se toma por ausencia de ese link, nunca por el contacto -> permite
crear la 2a, 3a... opp de un mismo contacto (force create), nunca update.

SUB-CASOS:
  (a) CREATE_OPP    : contacto en Marca + opp realmente ausente -> CREAR opp adicional.
  (b) BACKFILL_LINK : opp ya replicada en Marca pero sin el link CF poblado
                      (match unico por nombre+monto contra opps de Marca sin link)
                      -> NO crear; recomendar backfill_opp_sucursal_link.py.
  (c) REVIEW        : contacto no esta en Marca / huerfano / match ambiguo -> reportar.

Defaults seguros: DRY-RUN. --apply --run-id escribe en GHL con auditoria
(script_audit) reversible desde el dashboard, y snapshot pre-cambio en
generated/migrations/. Pilotar 1 sucursal con --location y/o 1 opp con --only-opp.

Uso:
  python scripts/create_missing_branch_opps_in_marca.py                 # dry-run, 3 sucursales
  python scripts/create_missing_branch_opps_in_marca.py --json          # salida JSON
  python scripts/create_missing_branch_opps_in_marca.py --location <id> # acota a 1 sucursal
  python scripts/create_missing_branch_opps_in_marca.py --only-opp <id> # acota a 1 opp (repetible)
  python scripts/create_missing_branch_opps_in_marca.py --apply --location <id> --only-opp <id> --run-id <uuid>
"""

import argparse
import json
import os
import sqlite3
import sys
import uuid
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)

from paths import DB_PATH, MIGRATIONS_DIR  # noqa: E402

# Reutilizamos la maquinaria probada de los scripts existentes.
import sync_engine  # noqa: E402
import script_audit  # noqa: E402
from common import match_contacts, normalize_phone, normalize_email, normalize_text  # noqa: E402

import audit_brand_vs_branches_totals as audit  # noqa: E402
import sync_missing_opps_to_brand as sm  # noqa: E402

BRAND_LOCATION_ID = audit.BRAND_LOCATION_ID
OPP_LINK_FIELD_KEY = audit.OPP_LINK_FIELD_KEY

# Sucursales objetivo (pull-auto, multi-empeno permitido). --location acota a una.
TARGET_BRANCHES = {
    "uZnMH5bO6MXTHcgHeyq9": "85935 - MP - Pilares",
    "nF1uEaYB3mCK5em9bPn2": "85974 - MP - Eugenia",
    "uJEn2iuUficuml9zxAnt": "85976 - MP - Cancún",
}


def _money_key(v):
    try:
        return round(float(v or 0), 2)
    except Exception:
        return 0.0


def _norm_name(s):
    return normalize_text(s or "")


def _branch_contact(conn, loc, contact_id):
    if not contact_id:
        return None
    r = conn.execute(
        "SELECT id, first_name, last_name, email, phone, tags, custom_fields_json "
        "FROM contacts WHERE location_id=? AND id=?",
        (loc, contact_id),
    ).fetchone()
    return dict(r) if r else None


def _full_name(c):
    if not c:
        return ""
    return ((c.get("first_name") or "") + " " + (c.get("last_name") or "")).strip()


def _has_link_to_branch_live(brand_opps_live, branch_opp_id):
    """True si alguna opp viva de Marca linkea (por CF) a branch_opp_id."""
    target = str(branch_opp_id)
    for bo in brand_opps_live or []:
        for cf in bo.get("customFields") or []:
            v = cf.get("value") or cf.get("fieldValue") or cf.get("fieldValueString")
            if v and str(v) == target:
                return True
    return False


def run_create(location_id=None, opp_ids=None, dry_run=True, run_id=None, log=None, json_out=False,
               branches_override=None):
    """Identifica y (en apply) crea las opps de sucursal faltantes en Marca.

    Args:
        location_id: acota a una sucursal. None = las 3 de TARGET_BRANCHES.
        opp_ids: acota a opp_ids de sucursal especificos (set/list). None = todas.
        dry_run: True (default) no escribe en GHL.
        run_id: id de script_audit (solo apply).
        log: callable log(line).
        branches_override: dict {location_id: name} para procesar un set explicito de
            sucursales (p.ej. TODAS via --all-branches). Tiene prioridad sobre location_id.
    Returns dict serializable.
    """
    if log is None:
        log = sm.safe_print
    if not os.path.exists(DB_PATH):
        raise FileNotFoundError(f"No existe {DB_PATH}. Corre una sincronizacion global primero.")

    if branches_override:
        branches = dict(branches_override)
    elif location_id:
        branches = {location_id: TARGET_BRANCHES.get(location_id, location_id)}
    else:
        branches = dict(TARGET_BRANCHES)
    wanted = set(opp_ids) if opp_ids else None

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

    tokens = sync_engine.get_tokens_map()
    brand_token = tokens.get(BRAND_LOCATION_ID)
    if not brand_token and not dry_run:
        raise RuntimeError("No se encontro token para la cuenta de Marca en el CSV de tokens.")

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

    summary = {
        "candidates": 0,
        "create_opp": 0,
        "backfill_link": 0,
        "review": 0,
        "skip_already_linked_live": 0,
        "opps_created": 0,
        "errors": 0,
    }
    items = []

    try:
        # --- Resolver field ids del link en Marca ---
        brand_opp_link_fid = audit.resolve_opp_link_field_id(conn, BRAND_LOCATION_ID)
        brand_contact_link_fid = audit.resolve_contact_link_field_id(conn, BRAND_LOCATION_ID)
        if not brand_opp_link_fid:
            raise RuntimeError("No se resolvio el field_id de 'ID Oportunidad Sucursal' en Marca (corre sync de metadata).")

        # Schemas para mapear CFs por nombre.
        brand_opp_schema_name_to_id = sm.load_schemas_name_to_id(conn, BRAND_LOCATION_ID, "opportunity")

        # --- Indices de Marca ---
        brand_opps = [dict(r) for r in conn.execute(
            "SELECT id, contact_id, status, name, monetary_value, custom_fields_json "
            "FROM opportunities WHERE location_id=?", (BRAND_LOCATION_ID,))]
        brand_link_idx = {}          # branch_opp_id -> brand_opp
        brand_unlinked_by_cid = {}   # brand_contact_id -> [brand_opp sin link]
        for o in brand_opps:
            lv = audit.extract_opp_link_value(o["custom_fields_json"], brand_opp_link_fid)
            if lv:
                brand_link_idx[str(lv)] = o
            else:
                brand_unlinked_by_cid.setdefault(o["contact_id"], []).append(o)

        brand_contacts = [dict(r) for r in conn.execute(
            "SELECT id, first_name, last_name, email, phone, custom_fields_json "
            "FROM contacts WHERE location_id=?", (BRAND_LOCATION_ID,))]
        bc_link_idx = {}  # branch_contact_id -> brand_contact
        bc_phone, bc_email = {}, {}
        for c in brand_contacts:
            cl = (audit.extract_contact_link_value(c["custom_fields_json"], brand_contact_link_fid)
                  if brand_contact_link_fid else None)
            if cl:
                bc_link_idx[str(cl)] = c
            p, e = normalize_phone(c.get("phone")), normalize_email(c.get("email"))
            if p:
                bc_phone.setdefault(p, []).append(c)
            if e:
                bc_email.setdefault(e, []).append(c)

        log(f"Indice Marca: {len(brand_contacts)} contactos, {len(brand_opps)} opps "
            f"({len(brand_link_idx)} con link).")

        # --- Recorrer sucursales ---
        if run_id and not dry_run:
            script_audit.create_run(run_id, "create_missing_branch_opps_in_marca",
                                    arguments=json.dumps({"location": location_id, "only_opp": list(wanted or [])}),
                                    locations=[BRAND_LOCATION_ID])

        for loc, bname in branches.items():
            branch_opp_schema_id_to_name = sm.load_schemas_id_to_name(conn, loc, "opportunity")
            opps = [dict(r) for r in conn.execute(
                "SELECT id, contact_id, status, name, monetary_value, pipeline_id, pipeline_stage_id, "
                "custom_fields_json FROM opportunities WHERE location_id=?", (loc,))]
            # multiplicidad por contacto en esta sucursal
            from collections import Counter
            mult = Counter(o["contact_id"] for o in opps if o["contact_id"])

            for o in opps:
                if str(o["id"]) in brand_link_idx:
                    continue  # ya replicada (strict-link)
                if wanted and o["id"] not in wanted:
                    continue
                summary["candidates"] += 1

                bcc = _branch_contact(conn, loc, o["contact_id"])
                item = {
                    "branch_location_id": loc,
                    "branch_name": bname,
                    "opp_id": o["id"],
                    "opp_name": o.get("name") or "",
                    "status": o.get("status") or "",
                    "monetary_value": _money_key(o.get("monetary_value")),
                    "contact_id": o.get("contact_id") or "",
                    "contact_name": _full_name(bcc) or "(huerfano)",
                    "multi_empeno": mult.get(o["contact_id"], 0) > 1,
                    "verdict": None,
                    "subcase": None,
                    "similarity": 0,
                    "match_method": None,
                    "brand_contact_id": None,
                    "notes": None,
                }

                # ---- mapear contacto sucursal -> Marca ----
                brand_contact = None
                if bcc:
                    if str(bcc["id"]) in bc_link_idx:
                        brand_contact = bc_link_idx[str(bcc["id"])]
                        item["match_method"], item["similarity"] = "link", 100
                    else:
                        cands, seen = [], set()
                        for c in bc_phone.get(normalize_phone(bcc.get("phone")), []):
                            if c["id"] not in seen:
                                cands.append(c); seen.add(c["id"])
                        for c in bc_email.get(normalize_email(bcc.get("email")), []):
                            if c["id"] not in seen:
                                cands.append(c); seen.add(c["id"])
                        best = None
                        for c in cands:
                            r = match_contacts(bcc, c)
                            if r["level"] in ("strong", "medium") and (best is None or r["score"] > best[1]["score"]):
                                best = (c, r)
                        if best:
                            brand_contact = best[0]
                            item["match_method"] = best[1]["level"]
                            item["similarity"] = 100 if best[1]["level"] == "strong" else round(best[1]["score"] * 100)

                if brand_contact:
                    item["brand_contact_id"] = brand_contact["id"]

                # ---- clasificar sub-caso ----
                if not bcc:
                    item["verdict"], item["subcase"] = "REVIEW", "ORPHAN_CONTACT"
                    item["notes"] = "Opp de sucursal sin contacto cacheado."
                elif not brand_contact:
                    item["verdict"], item["subcase"] = "REVIEW", "CONTACT_NOT_IN_MARCA"
                    item["notes"] = "El contacto no existe en Marca; crear contacto es delicado (auto-opp). Revisar manualmente."
                else:
                    tnm, tmk = _norm_name(o.get("name")), _money_key(o.get("monetary_value"))
                    unlinked = brand_unlinked_by_cid.get(brand_contact["id"], [])
                    matches = [u for u in unlinked if _norm_name(u["name"]) == tnm and _money_key(u["monetary_value"]) == tmk]
                    if len(matches) == 1:
                        item["verdict"], item["subcase"] = "BACKFILL_LINK", "REPLICATED_NO_LINK"
                        item["brand_opp_id_match"] = matches[0]["id"]
                        item["notes"] = (f"Ya existe en Marca (opp {matches[0]['id']}) sin link CF. "
                                         f"Correr: backfill_opp_sucursal_link.py --only-opp {matches[0]['id']}")
                    elif len(matches) > 1:
                        item["verdict"], item["subcase"] = "REVIEW", "AMBIGUOUS_MATCH"
                        item["notes"] = f"{len(matches)} opps de Marca sin link matchean nombre+monto; ambiguo, no auto-crear."
                    else:
                        item["verdict"] = "CREATE_OPP"
                        item["subcase"] = "MULTI_EMPENO" if item["multi_empeno"] else "SINGLE"

                # ---- contadores + accion ----
                if item["verdict"] == "BACKFILL_LINK":
                    summary["backfill_link"] += 1
                elif item["verdict"] == "REVIEW":
                    summary["review"] += 1
                elif item["verdict"] == "CREATE_OPP":
                    summary["create_opp"] += 1
                    bpid, bsid = sm.resolve_brand_pipeline_and_stage(
                        conn, loc, o.get("pipeline_id"), o.get("pipeline_stage_id"))
                    payload = sm.build_brand_opp_payload(
                        o, brand_contact["id"], branch_opp_schema_id_to_name,
                        brand_opp_schema_name_to_id, bpid, bsid)
                    # Reinyeccion defensiva del link CF = branch_opp_id (por nombre ya
                    # deberia venir el self-id, pero garantizamos que sea exacto).
                    cfs = [cf for cf in payload.get("customFields", []) if cf.get("id") != brand_opp_link_fid]
                    cfs.append({"id": brand_opp_link_fid, "key": OPP_LINK_FIELD_KEY, "field_value": o["id"]})
                    payload["customFields"] = cfs
                    item["pipeline_resolved"] = bool(bpid and bsid)
                    item["payload_preview"] = sm._preview_payload(payload)

                    if not item["pipeline_resolved"]:
                        item["verdict"], item["subcase"] = "REVIEW", "NO_PIPELINE"
                        item["notes"] = "No se resolvio pipeline/stage en Marca; no crear."
                        summary["create_opp"] -= 1
                        summary["review"] += 1
                    elif not dry_run:
                        try:
                            # Anti-duplicado en vivo: re-verificar que ninguna opp viva
                            # del contacto ya linkee a este branch_opp_id.
                            live = sm.fetch_brand_opps_for_contact_live(brand_token, brand_contact["id"])
                            if _has_link_to_branch_live(live, o["id"]):
                                item["status_result"] = "skip_already_linked_live"
                                summary["skip_already_linked_live"] += 1
                            else:
                                res = sync_engine.ghl_client.create_opportunity(brand_token, payload)
                                new_id = (res.get("opportunity") or {}).get("id") or res.get("id")
                                if not new_id:
                                    raise RuntimeError(f"GHL no devolvio id de opp creada: {res}")
                                summary["opps_created"] += 1
                                item["status_result"] = "created"
                                item["new_brand_opp_id"] = new_id
                                if run_id:
                                    cid = script_audit.record_change(
                                        run_id, BRAND_LOCATION_ID, "opportunity", new_id,
                                        "", "created", None, payload)
                                    if cid:
                                        script_audit.mark_change(cid, "applied")
                                try:
                                    sm.upsert_brand_opp_in_db(conn, new_id, payload, contact_id=brand_contact["id"])
                                    sync_engine.refresh_opportunity_in_db(brand_token, new_id, BRAND_LOCATION_ID)
                                except Exception as db_exc:
                                    log(f"  WARN: no se pudo refrescar SQLite: {db_exc}")
                        except Exception as exc:
                            summary["errors"] += 1
                            item["status_result"] = "error"
                            item["error"] = str(exc)
                            log(f"  ERROR creando opp {o['id']}: {exc}")

                items.append(item)
                log(f"  [{item['verdict']}/{item['subcase']}] {bname} | {item['opp_name'][:30]} | "
                    f"{item['contact_name'][:25]} | sim={item['similarity']}% {item['match_method'] or ''}")

        if run_id and not dry_run:
            script_audit.update_run_status(run_id, "completed")

        # --- snapshot pre/post-cambio ---
        os.makedirs(MIGRATIONS_DIR, exist_ok=True)
        snap_name = f"create_missing_branch_opps_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
        snap_path = os.path.join(MIGRATIONS_DIR, snap_name)
        with open(snap_path, "w", encoding="utf-8") as fh:
            json.dump({"dry_run": dry_run, "run_id": run_id, "summary": summary, "items": items},
                      fh, ensure_ascii=False, indent=2, default=str)
    finally:
        conn.close()

    result = {"dry_run": dry_run, "run_id": run_id, "summary": summary,
              "items": items, "snapshot": snap_path}
    log("\n=== RESUMEN ===")
    for k, v in summary.items():
        log(f"  {k}: {v}")
    log(f"Snapshot: {snap_path}")
    return result


def main():
    parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    parser.add_argument("--apply", action="store_true", help="Ejecuta las creaciones en GHL. Default dry-run.")
    parser.add_argument("--yes", action="store_true", help="Skip confirmacion interactiva (headless).")
    parser.add_argument("--location", type=str, default=None, help="Acota a una sucursal (location_id).")
    parser.add_argument("--all-branches", action="store_true", help="Procesa TODAS las sucursales del CSV (no solo las 3 default).")
    parser.add_argument("--only-opp", action="append", default=[], help="Procesa solo el opp_id de sucursal dado (repetible).")
    parser.add_argument("--run-id", type=str, default=None, help="Id de script_audit. Autogenera uno si se omite con --apply.")
    parser.add_argument("--json", action="store_true", help="Imprime el resultado como JSON al final.")
    args = parser.parse_args()

    dry_run = not args.apply
    run_id = args.run_id
    if not dry_run:
        if not args.yes:
            confirm = input("Esto CREARA opps en GHL (Marca). Continuar? (y/N): ").strip().lower()
            if confirm not in ("y", "yes", "s", "si"):
                print("Cancelado.")
                sys.exit(0)
        if not run_id:
            run_id = str(uuid.uuid4())
            print(f"run_id autogenerado: {run_id}")

    branches_override = None
    if args.all_branches:
        # Todas las sucursales del CSV, excluyendo Marca y las cuentas DEMO.
        DEMOS = {"Vf7qQl3L9vakJ8hDtQ8e", "Z64WQKORPVwXb5mn68Ef"}
        branches_override = {
            a["location_id"]: a.get("nombre") or a["location_id"]
            for a in sync_engine.parse_accounts_csv()
            if a["location_id"] != BRAND_LOCATION_ID and a["location_id"] not in DEMOS
        }
        sm.safe_print(f"--all-branches: {len(branches_override)} sucursales del CSV")

    try:
        result = run_create(
            location_id=args.location,
            opp_ids=args.only_opp or None,
            dry_run=dry_run,
            run_id=run_id,
            log=sm.safe_print,
            branches_override=branches_override,
        )
    except FileNotFoundError as e:
        sm.safe_print(f"ERROR: {e}")
        sys.exit(2)
    except RuntimeError as e:
        sm.safe_print(f"ERROR: {e}")
        sys.exit(3)

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


if __name__ == "__main__":
    main()
