#!/usr/bin/env python3
"""Alinea opciones de picklists divergentes en sucursales al estándar de Marca.

Origen: el audit cross-object (audit_custom_fields_cross_object.py) detectó que
varios picklists tienen las mismas opciones en Marca y sucursales pero con
casing distinto (o, peor, opciones con texto totalmente distinto). GHL guarda
el VALOR del picklist como string literal en cada record, así que cambiar el
label en el schema deja las opps existentes apuntando a un string que ya no
existe en el dropdown.

Este script:
    1. Para cada campo en la tabla `ALIGNMENTS`, lee el schema actual de la
       sucursal y construye un plan de cambios.
    2. Recorre las opps (o contacts) afectadas y arma un mapeo
       `valor_actual → valor_objetivo` usando la tabla VALUE_MAPPINGS del
       campo.
    3. En modo `--dry-run` (default) reporta el plan sin tocar nada.
    4. Con `--apply` ejecuta en este orden:
        a. Snapshot completo de schema + records afectados a
           `generated/migrations/picklist_align_<branch>_<field>_<ts>.json`.
        b. PUT al schema con las nuevas options.
        c. PUT a cada record afectado con el valor mapeado.
        d. Registro en `script_audit` (planned → applied / failed).
    5. Mapeos no-triviales (texto distinto, no solo casing) requieren el flag
       `--accept-ambiguous-mappings`. Sin él, se reportan en el plan pero NO
       se aplican.

Convención de seguridad:
    * Por default solo procesa el campo `opportunity.CANAL DE ORIGEN` (caso
      100% trivial — solo casing 1-a-1). Para más casos pasar `--field`.
    * Recomendado correr primero en `--location` de una sucursal canary
      (ej. La Viga) antes de `--all`.
    * El script NO recrea el field (no usa DELETE+POST). Solo hace PUT con
      `options`. Si el PUT no acepta `options`, abortar y usar el patrón
      DELETE+POST de migrate_branch_fieldkeys.py.

Uso típico:
    # 1) Plan en una sucursal:
    python scripts/fix_opportunity_picklist_alignment.py \\
        --location fKn9SaXZoKcjjLryg10v --field "CANAL DE ORIGEN"

    # 2) Aplicar en esa sucursal con run-id:
    python scripts/fix_opportunity_picklist_alignment.py \\
        --location fKn9SaXZoKcjjLryg10v --field "CANAL DE ORIGEN" \\
        --apply --run-id <uuid>

    # 3) Rollout a todas las sucursales una vez validado:
    python scripts/fix_opportunity_picklist_alignment.py \\
        --all --field "CANAL DE ORIGEN" --apply --run-id <uuid>
"""

import argparse
import datetime
import json
import os
import sys
import time
import unicodedata
import warnings

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

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

BASE_URL = "https://services.leadconnectorhq.com"
API_VERSION = "2021-07-28"
OBJECT_API_VERSION = "2021-04-15"
BRAND_LOCATION_ID = "GbKkBpCmKu2QmloKFHy3"


# ───────────────────────────────────────────────────────────────────────────
# Tabla de alineamientos
# ───────────────────────────────────────────────────────────────────────────
# Cada entrada describe un campo cuyas opciones en sucursales no coinciden con
# Marca. `target_options` es la lista exacta que debe quedar en la sucursal
# (orden incluido). `value_mappings` mapea cada string que pueda existir en
# records actuales hacia el valor objetivo. Mapeos marcados como `ambiguous`
# solo se aplican con --accept-ambiguous-mappings.

ALIGNMENTS = {
    "CANAL DE ORIGEN": {
        "object": "opportunity",
        "match_names": ["CANAL DE ORIGEN", "Canal de Origen"],
        "target_options": ["FORMULARIO", "FACEBOOK", "WHATSAPP", "LLAMADA", "INSTAGRAM", "SUCURSAL"],
        "value_mappings": {
            "Formulario": "FORMULARIO",
            "Facebook": "FACEBOOK",
            "WhatsApp": "WHATSAPP",
            "Whatsapp": "WHATSAPP",
            "Llamada": "LLAMADA",
            "Instagram": "INSTAGRAM",
            "Sucursal": "SUCURSAL",
        },
        "ambiguous_mappings": {},
    },
    "Fuente de Prospecto": {
        "object": "opportunity",
        "match_names": ["Fuente de Prospecto"],
        # Target alineado con DOCUMENTACIÓN de Monte Providencia.md líneas
        # 178-187 / 242-251: 9 opciones idénticas en contact y opportunity.
        # NO se separan "PROSPECCIÓN CAMPO"/"PROSPECCIÓN DIGITAL" (decisión
        # del owner del proyecto el 2026-05-26, ver chat).
        "target_options": [
            "CLIENTE CONOCIDO", "SUCURSAL", "PROSPECCIÓN", "REFERIDO",
            "ALIANZA", "EVENTO ESPECIAL", "LEAD DIGITAL", "REDES SOCIALES",
            "GALLARDETES",
        ],
        "value_mappings": {
            "Sucursal": "SUCURSAL",
            "Referido": "REFERIDO",
            "Alianza": "ALIANZA",
            "Evento especial": "EVENTO ESPECIAL",
            "Lead digital": "LEAD DIGITAL",
            "Cliente conocido": "CLIENTE CONOCIDO",
            "Prospección": "PROSPECCIÓN",
            "Redes sociales": "REDES SOCIALES",
            "Gallardetes": "GALLARDETES",
            # Mapeos confirmados por el owner del proyecto: consolidamos las
            # variantes "Campo"/"Digital" a PROSPECCIÓN y "Tienda" a REDES
            # SOCIALES, según el catálogo documentado.
            "Prospección campo": "PROSPECCIÓN",
            "Prospección digital": "PROSPECCIÓN",
            "Redes sociales Tienda": "REDES SOCIALES",
        },
        "ambiguous_mappings": {},
    },
    "Lead Descartado": {
        "object": "opportunity",
        "match_names": ["Lead Descartado"],
        # Todo en MAYÚSCULAS. "Sin interés" se consolida a "SIN RESPUESTA"
        # (decisión confirmada del owner el 2026-05-26 — el campo no tenía
        # records con valor en ninguna cuenta al momento del rollout, así que
        # la consolidación no afecta datos existentes).
        "target_options": ["NO CUMPLE REQUISITOS", "CLIENTE DECLINA", "SIN RESPUESTA", "N/A"],
        "value_mappings": {
            "No cumple requisitos": "NO CUMPLE REQUISITOS",
            "No Cumple Requisitos": "NO CUMPLE REQUISITOS",
            "no cumple requisitos": "NO CUMPLE REQUISITOS",
            "Cliente Declina": "CLIENTE DECLINA",
            "Cliente declina": "CLIENTE DECLINA",
            "Sin respuesta": "SIN RESPUESTA",
            "Sin interés": "SIN RESPUESTA",
        },
        "ambiguous_mappings": {},
    },
    "Sucursal (contact)": {
        "object": "contact",
        "match_names": ["Sucursal"],
        # 41 opciones de Marca (excluye "Tierra Colorada, Guerrero" — sucursal
        # eliminada). Se confirmó que ningún contacto tiene ese valor.
        "target_options": [
            "Atlacomulco, Estado de México", "Atizapán, Estado de México",
            "Altamira, Tamaulipas", "Cancún, Quintana Roo",
            "Ciudad del Carmen, Campeche", "Ciudad Satélite, Estado de México",
            "Chilpancingo, Guerrero", "Cuajimalpa, Ciudad de México",
            "Cuautla, Morelos", "Ecatepec, Estado de México",
            "Plaza el Salado, Ciudad de México", "Huauchinango, Puebla",
            "Interlomas, Estado de México", "Ixmiquilpan, Hidalgo",
            "Izcalli, Estado de México", "Jojutla, Morelos",
            "La Viga, Ciudad de México", "Marina Nacional, Ciudad de México",
            "Metepec, Estado de México", "Miacatlán, Morelos",
            "Miahuatlán, Oaxaca", "Morelia, Michoacán", "Pinotepa, Oaxaca",
            "Playa del Carmen, Quintana Roo", "Pochutla, Oaxaca",
            "Puebla, Puebla", "Querétaro, Querétaro", "Reynosa, Tamaulipas",
            "Tampico, Tamaulipas", "Tapachula, Chiapas", "Temixco, Morelos",
            "Texcoco, Estado de México", "Toluca, Estado de México",
            "Lerma, Estado de México", "Tulyehualco, Ciudad de México",
            "Tuxtla, Chiapas", "Uruapan, Michoacán", "Zacatepec, Morelos",
            "Zinacantepec, Estado de México", "Zitácuaro, Michoacán",
            "Narvarte Oriente, Ciudad de México",
        ],
        "value_mappings": {},
        "ambiguous_mappings": {},
    },
    "Canal de Origen (contact rename)": {
        "object": "contact",
        "match_names": ["Canal de Origen", "CANAL DE ORIGEN"],
        # El field ya existe en MAYÚSCULAS en las opciones (las sucursales
        # heredan de Marca). Solo cambiamos el NAME del field a "CANAL DE
        # ORIGEN" para alinear con su homólogo en opportunity.
        "target_name": "CANAL DE ORIGEN",
        "target_options": ["FORMULARIO", "FACEBOOK", "WHATSAPP", "LLAMADA", "INSTAGRAM", "SUCURSAL"],
        "value_mappings": {
            "Formulario": "FORMULARIO",
            "Facebook": "FACEBOOK",
            "WhatsApp": "WHATSAPP",
            "Whatsapp": "WHATSAPP",
            "Llamada": "LLAMADA",
            "Instagram": "INSTAGRAM",
            "Sucursal": "SUCURSAL",
        },
        "ambiguous_mappings": {},
    },
}


_last_request_by_token: dict = {}


def normalize_name(value):
    s = str(value or "").strip().lower()
    s = unicodedata.normalize("NFKD", s)
    s = "".join(c for c in s if not unicodedata.combining(c))
    return " ".join(s.split())


def wait_for_rate_limit(token):
    elapsed = time.time() - _last_request_by_token.get(token, 0)
    if elapsed < 0.110:
        time.sleep(0.110 - elapsed)
    _last_request_by_token[token] = time.time()


def ghl_request(method, endpoint, token, *, params=None, json_body=None,
                version=API_VERSION, retries=3):
    wait_for_rate_limit(token)
    url = endpoint if endpoint.startswith("http") else f"{BASE_URL}{endpoint}"
    headers = {
        "Accept": "application/json",
        "Authorization": f"Bearer {token}",
        "Version": version,
        "Content-Type": "application/json",
    }
    last_exc = None
    for attempt in range(retries):
        try:
            resp = requests.request(method, url, headers=headers, params=params,
                                    json=json_body, timeout=45)
        except requests.RequestException as exc:
            last_exc = exc
            time.sleep(0.5 * (attempt + 1))
            continue
        if resp.status_code == 429:
            time.sleep(1.0 * (attempt + 1))
            continue
        if resp.status_code >= 500:
            time.sleep(1.0 * (attempt + 1))
            continue
        resp.raise_for_status()
        if resp.status_code == 204:
            return {}
        return resp.json()
    if last_exc:
        raise last_exc


# ───────────────────────────────────────────────────────────────────────────
# Acceso a schema y records
# ───────────────────────────────────────────────────────────────────────────

def get_schema_field(location_id, token, object_key, match_names):
    """Localiza el field cuyo nombre normalizado coincide con cualquiera de
    `match_names`. Devuelve None si no existe."""
    ghl_request("GET", "/objects/", token, params={"locationId": location_id},
                version=OBJECT_API_VERSION)
    data = ghl_request("GET", f"/objects/{object_key}", token,
                       params={"locationId": location_id}, version=OBJECT_API_VERSION)
    fields = data.get("fields") or []
    target_norms = {normalize_name(n) for n in match_names}
    for f in fields:
        if normalize_name(f.get("name")) in target_norms:
            return f
    return None


def iter_records(object_key, location_id, token):
    if object_key == "opportunity":
        page = 1
        while True:
            data = ghl_request("POST", "/opportunities/search", token,
                               json_body={"locationId": location_id, "limit": 100, "page": page})
            batch = data.get("opportunities", []) or []
            if not batch:
                break
            for o in batch:
                yield o
            if len(batch) < 100:
                break
            page += 1
    else:
        from urllib.parse import parse_qs, urlparse
        start_after = None
        start_after_id = None
        while True:
            params = {"locationId": location_id, "limit": 100}
            if start_after:
                params["startAfter"] = start_after
            if start_after_id:
                params["startAfterId"] = start_after_id
            data = ghl_request("GET", "/contacts/", token, params=params)
            batch = data.get("contacts", []) or []
            if not batch:
                break
            for c in batch:
                yield c
            meta = data.get("meta") or {}
            cursor = meta.get("startAfter")
            cursor_id = meta.get("startAfterId")
            next_url = meta.get("nextPageUrl")
            if not cursor and next_url:
                qs = parse_qs(urlparse(next_url).query)
                cursor = (qs.get("startAfter") or [None])[0]
                cursor_id = cursor_id or (qs.get("startAfterId") or [None])[0]
            if not cursor and not cursor_id:
                break
            start_after = cursor
            start_after_id = cursor_id


def cf_value(record, field_id):
    for f in record.get("customFields", []) or []:
        if f.get("id") != field_id:
            continue
        for key in ("value", "fieldValue", "fieldValueString"):
            v = f.get(key)
            if v is not None:
                return v
        return None
    return None


# ───────────────────────────────────────────────────────────────────────────
# Mutaciones
# ───────────────────────────────────────────────────────────────────────────

def update_field_schema(location_id, token, field_id, *, target_options=None, target_name=None):
    """Actualiza opciones y/o el name de un custom field sin recrearlo.

    El endpoint `PUT /locations/{loc}/customFields/{id}` espera `options` como
    array de strings (NO objetos {key,label}). Pasarlo como dicts devuelve
    400 "v.trim is not a function". Verificado contra GHL en 2026-05.

    Si target_name está, se incluye en el body para renombrar el field. Si
    target_options es None, no se modifica la lista de opciones.
    """
    body = {}
    if target_options is not None:
        body["options"] = list(target_options)
    if target_name is not None:
        body["name"] = target_name
    if not body:
        return None
    return ghl_request("PUT", f"/locations/{location_id}/customFields/{field_id}",
                       token, json_body=body)


def put_record_field(object_key, location_id, token, record_id, field_id, value):
    body = {"customFields": [{"id": field_id, "value": value}]}
    endpoint = "/contacts/" if object_key == "contact" else "/opportunities/"
    return ghl_request("PUT", f"{endpoint}{record_id}", token, json_body=body)


# ───────────────────────────────────────────────────────────────────────────
# Snapshot
# ───────────────────────────────────────────────────────────────────────────

def write_snapshot(payload, branch_name, field_name):
    os.makedirs(MIGRATIONS_DIR, exist_ok=True)
    ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
    safe_branch = "".join(c if c.isalnum() else "_" for c in branch_name)[:40]
    safe_field = "".join(c if c.isalnum() else "_" for c in field_name)[:40]
    path = os.path.join(MIGRATIONS_DIR, f"picklist_align_{safe_branch}_{safe_field}_{ts}.json")
    with open(path, "w", encoding="utf-8") as fh:
        json.dump(payload, fh, ensure_ascii=False, indent=2, default=str)
    return path


# ───────────────────────────────────────────────────────────────────────────
# Núcleo: plan + apply por (branch, alignment)
# ───────────────────────────────────────────────────────────────────────────

def plan_alignment(branch, alignment_name, alignment):
    """Construye el plan sin tocar GHL en escritura. Solo lecturas."""
    object_key = alignment["object"]
    field = get_schema_field(branch["location_id"], branch["token"],
                             object_key, alignment["match_names"])
    plan = {
        "branch_name": branch["nombre"],
        "branch_location_id": branch["location_id"],
        "alignment_name": alignment_name,
        "object": object_key,
        "schema_field": None,
        "schema_changes_needed": False,
        "name_changes_needed": False,
        "current_name": None,
        "target_name": alignment.get("target_name"),
        "current_options": [],
        "target_options": alignment["target_options"],
        "value_mappings_applied": {},   # valor_viejo → valor_nuevo (no ambiguos)
        "value_mappings_ambiguous": {},  # valor_viejo → valor_nuevo (ambiguos)
        "records_to_update": [],         # [{id, current_value, new_value, ambiguous}]
        "records_with_unknown_value": [],  # valores que no están ni en target ni en mapping
        "errors": [],
    }
    if not field:
        plan["errors"].append("Field no encontrado en este branch.")
        return plan

    plan["schema_field"] = {
        "id": field["id"],
        "name": field.get("name"),
        "dataType": field.get("dataType"),
        "fieldKey": field.get("fieldKey"),
    }
    current = [o.get("label") for o in (field.get("options") or [])]
    plan["current_options"] = current
    plan["current_name"] = field.get("name")
    plan["schema_changes_needed"] = (current != alignment["target_options"])
    if alignment.get("target_name") and field.get("name") != alignment["target_name"]:
        plan["name_changes_needed"] = True

    # Recorrer records solo si hay diferencias. Si schema ya coincide pero queremos
    # validar, igual recorremos para asegurar consistencia de valores.
    target_set = set(alignment["target_options"])
    vmap = alignment["value_mappings"]
    amap = alignment["ambiguous_mappings"]

    for rec in iter_records(object_key, branch["location_id"], branch["token"]):
        val = cf_value(rec, field["id"])
        if val is None or val == "" or val == []:
            continue
        # Picklist single-options: string. Multi-options: lista. Manejamos
        # ambos haciendo la migración elemento-a-elemento.
        is_list = isinstance(val, list)
        vals = val if is_list else [val]
        new_vals = []
        ambiguous_hit = False
        unknown_hit = False
        for v in vals:
            vs = str(v)
            if vs in target_set:
                new_vals.append(vs)
                continue
            if vs in vmap:
                new_vals.append(vmap[vs])
                plan["value_mappings_applied"][vs] = vmap[vs]
                continue
            if vs in amap:
                new_vals.append(amap[vs])
                plan["value_mappings_ambiguous"][vs] = amap[vs]
                ambiguous_hit = True
                continue
            # Caso desconocido: dejamos el valor original
            new_vals.append(vs)
            unknown_hit = True

        new_value = new_vals if is_list else new_vals[0]
        if unknown_hit:
            plan["records_with_unknown_value"].append({
                "record_id": rec.get("id"),
                "current_value": val,
            })
        if new_value != val:
            plan["records_to_update"].append({
                "record_id": rec.get("id"),
                "current_value": val,
                "new_value": new_value,
                "ambiguous": ambiguous_hit,
            })

    return plan


def apply_plan(branch, alignment, plan, *, accept_ambiguous, dry_run, run_id):
    """Ejecuta el plan. Devuelve dict con stats."""
    stats = {
        "schema_updated": False,
        "records_updated": 0,
        "records_skipped_ambiguous": 0,
        "errors": [],
    }
    object_key = alignment["object"]
    location_id = branch["location_id"]
    token = branch["token"]
    field_id = plan["schema_field"]["id"]

    # === Snapshot ===
    snapshot_payload = {
        "branch": branch["nombre"],
        "branch_location_id": location_id,
        "alignment_name": plan["alignment_name"],
        "object": object_key,
        "field_id": field_id,
        "field_name": plan["schema_field"]["name"],
        "current_options": plan["current_options"],
        "target_options": plan["target_options"],
        "value_mappings_applied": plan["value_mappings_applied"],
        "value_mappings_ambiguous": plan["value_mappings_ambiguous"],
        "records_to_update": plan["records_to_update"],
        "records_with_unknown_value": plan["records_with_unknown_value"],
        "dry_run": dry_run,
        "timestamp_utc": datetime.datetime.now(datetime.timezone.utc).isoformat(),
    }
    snap_path = write_snapshot(snapshot_payload, branch["nombre"], plan["alignment_name"])
    print(f"      Snapshot → {snap_path}")

    if dry_run:
        return stats

    # === PUT al schema ===
    if plan["schema_changes_needed"] or plan["name_changes_needed"]:
        change_id = None
        if run_id:
            change_id = script_audit.record_change(
                run_id, location_id, "__schema__", field_id, field_id,
                plan["schema_field"]["name"],
                {"name": plan["current_name"], "options": plan["current_options"]},
                {"name": plan.get("target_name") or plan["current_name"],
                 "options": plan["target_options"]},
            )
        try:
            update_field_schema(
                location_id, token, field_id,
                target_options=(plan["target_options"] if plan["schema_changes_needed"] else None),
                target_name=(plan["target_name"] if plan["name_changes_needed"] else None),
            )
            if change_id:
                script_audit.mark_change(change_id, "applied")
            stats["schema_updated"] = True
            msg_parts = []
            if plan["schema_changes_needed"]:
                msg_parts.append(f"options {plan['current_options']} → {plan['target_options']}")
            if plan["name_changes_needed"]:
                msg_parts.append(f"name {plan['current_name']!r} → {plan['target_name']!r}")
            print(f"      ✓ Schema PUT: " + " | ".join(msg_parts))
        except Exception as exc:
            if change_id:
                script_audit.mark_change(change_id, "failed", str(exc))
            stats["errors"].append(f"PUT schema falló: {exc}")
            print(f"      ✗ PUT schema falló: {exc}. Snapshot preservado. Abortar antes de tocar records.")
            return stats

    # === PUT a cada record afectado ===
    for r in plan["records_to_update"]:
        if r["ambiguous"] and not accept_ambiguous:
            stats["records_skipped_ambiguous"] += 1
            continue
        if not script_audit.wait_if_paused_or_stopped(run_id):
            print("      Detención solicitada — aborto.")
            break
        change_id = None
        if run_id:
            change_id = script_audit.record_change(
                run_id, location_id, object_key, r["record_id"], field_id,
                plan["schema_field"]["name"], r["current_value"], r["new_value"],
            )
        try:
            put_record_field(object_key, location_id, token, r["record_id"], field_id, r["new_value"])
            if change_id:
                script_audit.mark_change(change_id, "applied")
            stats["records_updated"] += 1
        except Exception as exc:
            if change_id:
                script_audit.mark_change(change_id, "failed", str(exc))
            stats["errors"].append({"record_id": r["record_id"], "error": str(exc)})

    return stats


# ───────────────────────────────────────────────────────────────────────────
# Reporte
# ───────────────────────────────────────────────────────────────────────────

def print_plan(plan):
    print(f"  [Plan] {plan['branch_name']} ({plan['branch_location_id']}) — {plan['alignment_name']}")
    if plan["errors"]:
        for e in plan["errors"]:
            print(f"      ✗ {e}")
        return
    f = plan["schema_field"]
    print(f"      field id={f['id']}  name={f['name']!r}  dataType={f['dataType']}")
    print(f"      current options: {plan['current_options']}")
    print(f"      target  options: {plan['target_options']}")
    if plan.get("target_name"):
        print(f"      current name: {plan['current_name']!r}")
        print(f"      target  name: {plan['target_name']!r}")
    if plan["schema_changes_needed"] or plan["name_changes_needed"]:
        bits = []
        if plan["schema_changes_needed"]: bits.append("options")
        if plan["name_changes_needed"]: bits.append("name")
        print(f"      → schema necesita PUT ({', '.join(bits)})")
    else:
        print(f"      → schema ya alineado")
    if plan["value_mappings_applied"]:
        print(f"      mapeos triviales aplicables: {plan['value_mappings_applied']}")
    if plan["value_mappings_ambiguous"]:
        print(f"      ⚠ mapeos AMBIGUOS (requieren --accept-ambiguous-mappings):")
        for k, v in plan["value_mappings_ambiguous"].items():
            print(f"          {k!r} → {v!r}")
    print(f"      records a actualizar: {len(plan['records_to_update'])} "
          f"(de los cuales {sum(1 for r in plan['records_to_update'] if r['ambiguous'])} ambiguos)")
    if plan["records_with_unknown_value"]:
        print(f"      ⚠ records con valor desconocido (sin mapping definido): "
              f"{len(plan['records_with_unknown_value'])}")
        for r in plan["records_with_unknown_value"][:5]:
            print(f"          {r['record_id']}: {r['current_value']!r}")


# ───────────────────────────────────────────────────────────────────────────
# CLI
# ───────────────────────────────────────────────────────────────────────────

def select_targets(args, accounts):
    if args.location:
        matches = [a for a in accounts if a["location_id"] == args.location]
        if not matches:
            raise SystemExit(f"Location {args.location} no encontrada en CSV.")
        return matches
    if args.all:
        # Excluir Marca: el script opera sobre sucursales (Marca es source of truth).
        return [a for a in accounts if a["location_id"] != BRAND_LOCATION_ID]
    raise SystemExit("Especifica --location <id> o --all.")


def main():
    parser = argparse.ArgumentParser(
        description="Alinea opciones de picklists de sucursales al estándar de Marca.",
    )
    parser.add_argument("--location", help="Procesar solo esta sucursal.")
    parser.add_argument("--all", action="store_true",
                        help="Procesar todas las sucursales (excluye Marca).")
    parser.add_argument("--field", choices=list(ALIGNMENTS.keys()),
                        default="CANAL DE ORIGEN",
                        help="Campo a alinear (default: CANAL DE ORIGEN, el único 100%% trivial).")
    parser.add_argument("--accept-ambiguous-mappings", action="store_true",
                        help="Aplica también los mapeos con texto semánticamente distinto.")
    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.")
    args = parser.parse_args()

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

    if args.run_id:
        script_audit.init_audit_db()

    dry_run = not args.apply

    alignment_name = args.field
    alignment = ALIGNMENTS[alignment_name]

    accounts = sync_engine.parse_accounts_csv()
    targets = select_targets(args, accounts)

    print(f"Modo: {'DRY-RUN' if dry_run else 'APPLY'}")
    print(f"Campo: {alignment_name} ({alignment['object']})")
    print(f"Target options: {alignment['target_options']}")
    print(f"Sucursales en scope: {len(targets)}")
    if args.accept_ambiguous_mappings:
        print("⚠ ambiguous mappings ACTIVADOS")
    print()

    summary = []
    for branch in targets:
        if not script_audit.wait_if_paused_or_stopped(args.run_id):
            print("Detención solicitada — saliendo.")
            break
        try:
            plan = plan_alignment(branch, alignment_name, alignment)
        except Exception as exc:
            print(f"  [Plan] {branch['nombre']}: ERROR — {exc}")
            summary.append({"branch": branch["nombre"], "error": str(exc)})
            continue
        print_plan(plan)
        if plan["errors"] or not plan["schema_field"]:
            summary.append({"branch": branch["nombre"], "skipped": True})
            continue
        try:
            stats = apply_plan(branch, alignment, plan,
                               accept_ambiguous=args.accept_ambiguous_mappings,
                               dry_run=dry_run, run_id=args.run_id)
            summary.append({
                "branch": branch["nombre"],
                "schema_updated": stats["schema_updated"],
                "records_updated": stats["records_updated"],
                "records_skipped_ambiguous": stats["records_skipped_ambiguous"],
                "errors": stats["errors"],
            })
            print(f"      ✓ records_updated={stats['records_updated']} "
                  f"schema_updated={stats['schema_updated']} "
                  f"skipped_ambiguous={stats['records_skipped_ambiguous']}")
        except Exception as exc:
            print(f"      ✗ apply falló: {exc}")
            summary.append({"branch": branch["nombre"], "apply_error": str(exc)})

    # Resumen final
    print("\n" + "=" * 60)
    if dry_run:
        print("DRY-RUN — no se modificó nada. Revisa los snapshots y vuelve a correr con --apply.")
    total_updated = sum(s.get("records_updated", 0) for s in summary)
    total_skipped = sum(s.get("records_skipped_ambiguous", 0) for s in summary)
    print(f"Sucursales procesadas: {len(summary)}")
    print(f"Records actualizados : {total_updated}")
    print(f"Records ambiguos saltados: {total_skipped}")


if __name__ == "__main__":
    main()
