#!/usr/bin/env python3
"""Step 1: update Canal de Origen from contact tags, then opportunity fields."""

import argparse
import os
import re
import sys
import time
import warnings
from urllib.parse import parse_qs, urlparse

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 urllib.parse import quote  # noqa: E402
from canal_origen_resolver import (  # noqa: E402
    CANAL_BY_TAG,
    TARGET_TAGS as RESOLVER_TARGET_TAGS,
    decide as resolve_canal_decision,
)
import common  # noqa: E402  # FIELD_ALIASES + normalize_name

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

TARGET_TAGS = list(RESOLVER_TARGET_TAGS)

TAG_TO_CONTACT_VALUE = dict(CANAL_BY_TAG)

TAG_TO_OPP_UPDATES = {
    "facebook-ads": {
        "Canal de Origen de la Oportunidad": "Facebook",
        "Tipo de Lead": "Lead digital",
        "Fuente de Prospecto": "LEAD DIGITAL",
    },
    "sucursal": {
        "Canal de Origen de la Oportunidad": "Sucursal",
        "Fuente de Prospecto": "SUCURSAL",
    },
    "formulario": {
        "Canal de Origen de la Oportunidad": "Formulario",
        "Tipo de Lead": "LEAD DIGITAL",
        "Fuente de Prospecto": "LEAD DIGITAL",
    },
}

# Mapea los display names usados en TAG_TO_OPP_UPDATES a las alias keys de
# common.FIELD_ALIASES. Esto permite resolver el campo aunque la sucursal lo
# tenga renombrado/en mayúsculas (ej "CANAL DE ORIGEN").
OPP_FIELD_ALIAS_KEY = {
    "Canal de Origen de la Oportunidad": "canal_origen",
    "Tipo de Lead": "tipo_lead",
    "Fuente de Prospecto": "fuente_prospecto",
}


def resolve_opp_field_ids(opp_fields_list, field_display_name):
    """Devuelve TODOS los IDs del schema de oportunidad que matcheen con el
    display name (o su alias en common.FIELD_ALIASES). Maneja duplicados
    intra-location (mismo name, IDs distintos)."""
    alias_key = OPP_FIELD_ALIAS_KEY.get(field_display_name, field_display_name)
    candidate_names = common.FIELD_ALIASES.get(alias_key, [field_display_name])
    target_norm = {common.normalize_name(name) for name in candidate_names}
    ids = []
    for field in opp_fields_list or []:
        fname = field.get("name")
        fid = field.get("id")
        if not fname or not fid:
            continue
        if common.normalize_name(fname) in target_norm:
            ids.append(fid)
    return ids

TAGS_AUDIT_FIELD_ID = "__tags__"

_last_request_by_token = {}


def wait_for_rate_limit(token):
    now = time.time()
    elapsed = now - _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):
    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",
    }
    response = requests.request(method, url, headers=headers, params=params, json=json_body, timeout=45)
    response.raise_for_status()
    if response.status_code == 204:
        return {}
    return response.json()


def load_locations(include_main=False):
    accounts = sync_engine.parse_accounts_csv()
    if not include_main:
        accounts = [acc for acc in accounts if acc.get("type") == "branch"]
    return accounts


def get_main_account():
    accounts = load_locations(include_main=True)
    return next((acc for acc in accounts if acc["location_id"] == MAIN_LOCATION_ID), None)


def select_locations(args):
    if args.location:
        accounts = load_locations(include_main=True)
        matches = [acc for acc in accounts if acc["location_id"] == args.location]
        if not matches:
            raise SystemExit(f"Location {args.location} not found in CSV")
        return matches
    accounts = load_locations(include_main=args.include_main)
    if args.all:
        return accounts
    raise SystemExit("Specify --location <id> or --all. Use --dry-run to preview without changes.")


def get_object_schema(location_id, token, object_key):
    get_objects_catalog(location_id, token)
    data = ghl_request(
        "GET",
        f"/objects/{object_key}",
        token,
        params={"locationId": location_id},
        version=OBJECT_API_VERSION,
    )
    return {field["name"]: field["id"] for field in data.get("fields", []) if field.get("name") and field.get("id")}


def get_objects_catalog(location_id, token):
    """Fetches the objects catalog first so schema mapping starts from GHL metadata."""
    try:
        return ghl_request("GET", "/objects/", token, params={"locationId": location_id}, version=OBJECT_API_VERSION)
    except Exception:
        return {}


def get_schemas(location_id, token, *object_keys):
    return {object_key: get_object_schema(location_id, token, object_key) for object_key in object_keys}


def get_all_contacts(location_id, token):
    contacts = []
    seen_contact_ids = set()
    seen_cursors = set()
    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", [])
        if not batch:
            break

        new_contacts = 0
        for contact in batch:
            contact_id = contact.get("id")
            if not contact_id or contact_id in seen_contact_ids:
                continue
            seen_contact_ids.add(contact_id)
            contacts.append(contact)
            new_contacts += 1

        if new_contacts == 0:
            break

        meta = data.get("meta") or {}
        next_page_url = meta.get("nextPageUrl")
        cursor_after = meta.get("startAfter")
        cursor_id = meta.get("startAfterId")
        if not cursor_after and next_page_url:
            qs = parse_qs(urlparse(next_page_url).query)
            cursor_after = (qs.get("startAfter") or [None])[0]
            cursor_id = cursor_id or (qs.get("startAfterId") or [None])[0]
        if not cursor_after and not cursor_id:
            break
        cursor_key = f"{cursor_after}|{cursor_id}"
        if cursor_key in seen_cursors:
            break
        seen_cursors.add(cursor_key)
        start_after = cursor_after
        start_after_id = cursor_id

    return contacts


def get_latest_opportunity(location_id, contact_id, token):
    # GHL requiere snake_case (location_id / contact_id) en GET /opportunities/search.
    # El POST de búsqueda no acepta contactId en el body (devuelve 422).
    data = ghl_request(
        "GET",
        "/opportunities/search",
        token,
        params={"location_id": location_id, "contact_id": contact_id, "limit": 1},
    )
    opportunities = data.get("opportunities", [])
    return opportunities[0].get("id") if opportunities else None


def get_all_opportunities_for_contact(location_id, contact_id, token):
    """Paginates every opportunity associated with a contact in the location."""
    opportunities = []
    start_after = None
    start_after_id = None
    while True:
        params = {"location_id": location_id, "contact_id": contact_id, "limit": 100}
        if start_after is not None:
            params["startAfter"] = start_after
        if start_after_id:
            params["startAfterId"] = start_after_id
        data = ghl_request("GET", "/opportunities/search", token, params=params)
        batch = data.get("opportunities", []) or []
        if not batch:
            break
        opportunities.extend(batch)
        meta = data.get("meta") or {}
        total = meta.get("total") or 0
        if total and len(opportunities) >= total:
            break
        next_page = meta.get("nextPage")
        if next_page == "" or next_page is None:
            break
        start_after = meta.get("startAfter")
        start_after_id = meta.get("startAfterId")
        if start_after is None and not start_after_id:
            break
    return opportunities


def update_contact_field(contact_id, field_id, value, token):
    ghl_request(
        "PUT",
        f"/contacts/{contact_id}",
        token,
        json_body={"customFields": [{"id": field_id, "value": value}]},
    )


def add_contact_tags(contact_id, tags, token):
    ghl_request(
        "POST",
        f"/contacts/{contact_id}/tags",
        token,
        json_body={"tags": tags},
    )


def remove_contact_tag(contact_id, tag_name, token, location_id):
    # GHL acepta DELETE /contacts/{id}/tags con body {"tags":[...]} (el endpoint
    # path-based /tags/{name} responde 404 en la versión actual de la API).
    try:
        ghl_request(
            "DELETE",
            f"/contacts/{contact_id}/tags",
            token,
            json_body={"tags": [tag_name]},
        )
    except requests.HTTPError as exc:
        if exc.response is not None and exc.response.status_code == 404:
            ghl_request(
                "DELETE",
                f"/contacts/{contact_id}/tags/{quote(tag_name, safe='')}",
                token,
                params={"locationId": location_id},
            )
        else:
            raise


def safe_add_contact_tag(run_id, location_id, contact, tag, token, dry_run=False):
    tags_lower = {str(t).strip().lower() for t in contact.get("tags") or []}
    if tag in tags_lower:
        return False
    new_tags = sorted(tags_lower | {tag})
    change_id = script_audit.record_change(
        run_id, location_id, "contact", contact["id"], TAGS_AUDIT_FIELD_ID, "tags",
        sorted(tags_lower), new_tags,
    )
    if dry_run:
        contact.setdefault("tags", []).append(tag)
        return True
    try:
        add_contact_tags(contact["id"], [tag], token)
        script_audit.mark_change(change_id, "applied")
        contact.setdefault("tags", []).append(tag)
        return True
    except Exception as exc:
        script_audit.mark_change(change_id, "failed", str(exc))
        raise


def safe_remove_contact_tag(run_id, location_id, contact, tag, token, dry_run=False):
    original = [str(t) for t in contact.get("tags") or []]
    lower_to_original = {t.strip().lower(): t for t in original}
    if tag not in lower_to_original:
        return False
    remaining = [t for t in original if t.strip().lower() != tag]
    change_id = script_audit.record_change(
        run_id, location_id, "contact", contact["id"], TAGS_AUDIT_FIELD_ID, "tags",
        original, remaining,
    )
    if dry_run:
        contact["tags"] = remaining
        return True
    try:
        remove_contact_tag(contact["id"], lower_to_original[tag], token, location_id)
        script_audit.mark_change(change_id, "applied")
        contact["tags"] = remaining
        return True
    except Exception as exc:
        script_audit.mark_change(change_id, "failed", str(exc))
        raise


def update_opportunity_fields(opportunity_id, field_updates, token):
    ghl_request(
        "PUT",
        f"/opportunities/{opportunity_id}",
        token,
        json_body={"customFields": field_updates},
    )


def get_matching_tag(tags):
    normalized = {str(tag).lower() for tag in tags or []}
    for tag in TARGET_TAGS:
        if tag in normalized:
            return tag
    return None


def get_congruent_values(fuente_clean, source):
    if not source or not fuente_clean:
        return None, None
    src_lower = str(source).strip().lower()
    if fuente_clean == "LEAD DIGITAL":
        if any(word in src_lower for word in ["facebook", "ads", "ig", "instagram"]):
            return "facebook-ads", "FACEBOOK"
        elif any(word in src_lower for word in ["form", "formulario", "web", "landing", "page", "survey", "encuesta", "qr", "chat", "widget", "inbound", "social", "tiktok", "whatsapp", "wa.me", "api"]):
            return "formulario", "FORMULARIO"
    elif fuente_clean == "SUCURSAL":
        if any(word in src_lower for word in ["sucursal", "manual", "tienda", "offline"]):
            return "sucursal", "SUCURSAL"
    return None, None


def is_source_congruent_with_formulario(source):
    _, canal = get_congruent_values("LEAD DIGITAL", source)
    return canal == "FORMULARIO"


def is_source_congruent_with_sucursal(source):
    _, canal = get_congruent_values("SUCURSAL", source)
    return canal == "SUCURSAL"


def contact_display_name(contact):
    return (
        contact.get("contactName")
        or " ".join(filter(None, [contact.get("firstName"), contact.get("lastName")]))
        or contact.get("email")
        or contact.get("phone")
        or contact.get("id")
    )


def normalize_phone(phone):
    digits = re.sub(r"\D+", "", str(phone or ""))
    if len(digits) > 10:
        digits = digits[-10:]
    return digits


def normalize_email(email):
    return str(email or "").strip().lower()


def normalize_name(contact):
    name = contact_display_name(contact)
    return " ".join(str(name or "").strip().lower().split())


def build_contact_match_index(contacts):
    index = {"phone": {}, "email": {}, "name": {}}
    for contact in contacts:
        phone = normalize_phone(contact.get("phone"))
        email = normalize_email(contact.get("email"))
        name = normalize_name(contact)
        if phone:
            index["phone"].setdefault(phone, []).append(contact)
        if email:
            index["email"].setdefault(email, []).append(contact)
        if name:
            index["name"].setdefault(name, []).append(contact)
    return index


def find_exact_main_contact(branch_contact, main_index):
    match_order = (
        ("phone", normalize_phone(branch_contact.get("phone")), "teléfono"),
        ("email", normalize_email(branch_contact.get("email")), "correo"),
        ("name", normalize_name(branch_contact), "nombre"),
    )
    for key, value, label in match_order:
        if not value:
            continue
        matches = main_index[key].get(value, [])
        if len(matches) == 1:
            return matches[0], label
        if len(matches) > 1:
            print(f"  SKIP marca principal: match ambiguo por {label} para {contact_display_name(branch_contact)}")
            return None, label
    return None, None


def get_custom_field_value(record, field_id):
    # GHL devuelve customFields con keys distintas según el objeto:
    #   contacts → "value"
    #   opportunities → "fieldValue"
    # Algunos endpoints históricos también devuelven "fieldValueString".
    for field in record.get("customFields", []) or []:
        if field.get("id") != field_id:
            continue
        for key in ("value", "fieldValue", "fieldValueString"):
            val = field.get(key)
            if val is not None:
                return val
        return None
    return None


def get_opportunity(location_id, opportunity_id, token):
    # GHL envuelve la respuesta en {"opportunity": {...}, "traceId": "..."}.
    # Desempaquetamos para que get_custom_field_value pueda leer customFields.
    try:
        data = ghl_request("GET", f"/opportunities/{opportunity_id}", token, params={"locationId": location_id})
    except requests.HTTPError:
        return {}
    inner = data.get("opportunity")
    return inner if isinstance(inner, dict) else data


def safe_update_contact_field(run_id, location_id, contact, field_id, field_name, value, token, dry_run=False):
    old_value = get_custom_field_value(contact, field_id)
    if old_value == value:
        return False
    change_id = script_audit.record_change(run_id, location_id, "contact", contact["id"], field_id, field_name, old_value, value)
    if dry_run:
        return True
    try:
        update_contact_field(contact["id"], field_id, value, token)
        script_audit.mark_change(change_id, "applied")
        return True
    except Exception as exc:
        script_audit.mark_change(change_id, "failed", str(exc))
        raise


def safe_update_opportunity_field(run_id, location_id, opportunity_id, opportunity, field_id, field_name, value, token, dry_run=False):
    old_value = get_custom_field_value(opportunity, field_id)
    if old_value == value:
        return False
    change_id = script_audit.record_change(run_id, location_id, "opportunity", opportunity_id, field_id, field_name, old_value, value)
    if dry_run:
        return True
    try:
        update_opportunity_fields(opportunity_id, [{"id": field_id, "value": value}], token)
        script_audit.mark_change(change_id, "applied")
        return True
    except Exception as exc:
        script_audit.mark_change(change_id, "failed", str(exc))
        raise


def process_location(account, dry_run=False, contact_only=False, run_id=None, sync_main=False,
                     clean_mix=True, infer_from_source=True):
    location_id = account["location_id"]
    token = account["token"]
    name = account["nombre"]

    schemas = get_schemas(location_id, token, "contact", "opportunity")
    contact_schema = schemas["contact"]
    opportunity_schema = {} if contact_only else schemas["opportunity"]
    # Lista cruda de opp fields para resolver duplicados y aliases en escrituras.
    opp_fields_list = [] if contact_only else sync_engine.ghl_client.get_object_schema_fields(token, location_id, "opportunity")

    main_account = get_main_account() if sync_main and location_id != MAIN_LOCATION_ID else None
    main_contact_schema = {}
    main_opportunity_schema = {}
    main_opp_fields_list = []
    main_contacts = []
    main_index = None
    if main_account:
        main_schemas = get_schemas(main_account["location_id"], main_account["token"], "contact", "opportunity")
        main_contact_schema = main_schemas["contact"]
        main_opportunity_schema = {} if contact_only else main_schemas["opportunity"]
        if not contact_only:
            main_opp_fields_list = sync_engine.ghl_client.get_object_schema_fields(
                main_account["token"], main_account["location_id"], "opportunity"
            )
        main_contacts = get_all_contacts(main_account["location_id"], main_account["token"])
        main_index = build_contact_match_index(main_contacts)

    contact_canal_id = contact_schema.get("Canal de Origen")
    contact_fuente_id = contact_schema.get("Fuente de Prospecto")
    if not contact_canal_id:
        print(f"\n{name} - missing contact field: Canal de Origen")
        return 0

    contacts = get_all_contacts(location_id, token)
    updated_contacts = 0
    updated_opportunities = 0
    skipped_no_opportunity = 0
    review_needed = 0
    tag_changes = 0

    for contact in contacts:
        if not script_audit.wait_if_paused_or_stopped(run_id):
            print("\nDetención segura solicitada. Saliendo antes del siguiente contacto.")
            break

        fuente_val = get_custom_field_value(contact, contact_fuente_id) if contact_fuente_id else None
        canal_val = get_custom_field_value(contact, contact_canal_id)
        source = contact.get("source")

        decision = resolve_canal_decision(
            contact.get("tags") or [], fuente_val, canal_val, source,
        )

        # Si no resolvimos por tags+canal+source+fuente, intentar con la oportunidad asociada
        # antes de marcar REVIEW. Solo aplica cuando el schema de oportunidad está disponible.
        if not decision["decided_tag"] and opportunity_schema:
            opp_canal_field_id = opportunity_schema.get("Canal de Origen de la Oportunidad")
            if opp_canal_field_id:
                opp_id = get_latest_opportunity(location_id, contact.get("id"), token)
                if opp_id:
                    opp = get_opportunity(location_id, opp_id, token)
                    opp_canal_value = get_custom_field_value(opp, opp_canal_field_id)
                    if opp_canal_value:
                        decision = resolve_canal_decision(
                            contact.get("tags") or [], fuente_val, canal_val, source,
                            opportunity_canal_value=opp_canal_value,
                        )

        if decision["review"]:
            review_needed += 1
            display = contact_display_name(contact)
            print(
                f"  REVIEW {display} | tags={decision['present_targets']} "
                f"canal={canal_val} fuente={fuente_val} source={source} :: {decision['reason']}"
            )
            continue

        if not decision["decided_tag"]:
            continue

        matched_tag = decision["decided_tag"]
        canal_value = decision["canal_value"]

        if infer_from_source and decision["tags_to_add"]:
            for tag_to_add in decision["tags_to_add"]:
                try:
                    if safe_add_contact_tag(run_id, location_id, contact, tag_to_add, token, dry_run):
                        tag_changes += 1
                        print(
                            f"  TAG+ {contact_display_name(contact)} +{tag_to_add} "
                            f"({decision['reason']})"
                        )
                except Exception as exc:
                    print(f"    [WARN] Error añadiendo tag '{tag_to_add}': {exc}")

        if clean_mix and decision["tags_to_remove"]:
            for tag_to_remove in decision["tags_to_remove"]:
                try:
                    if safe_remove_contact_tag(run_id, location_id, contact, tag_to_remove, token, dry_run):
                        tag_changes += 1
                        print(
                            f"  TAG- {contact_display_name(contact)} -{tag_to_remove} "
                            f"(conserva '{matched_tag}')"
                        )
                except Exception as exc:
                    print(f"    [WARN] Error removiendo tag '{tag_to_remove}': {exc}")

        contact_id = contact.get("id")

        changed = safe_update_contact_field(
            run_id, location_id, contact, contact_canal_id, "Canal de Origen",
            canal_value, token, dry_run,
        )
        if changed:
            updated_contacts += 1
            print(f"  OK {contact_display_name(contact)} | {matched_tag} -> Canal de Origen: {canal_value}")

        main_contact = None
        if main_account and main_index and main_contact_schema.get("Canal de Origen"):
            main_contact, match_type = find_exact_main_contact(contact, main_index)
            if main_contact:
                if infer_from_source and decision["tags_to_add"]:
                    for tag_to_add in decision["tags_to_add"]:
                        try:
                            if safe_add_contact_tag(
                                run_id, main_account["location_id"], main_contact,
                                tag_to_add, main_account["token"], dry_run,
                            ):
                                print(
                                    f"  TAG+ marca principal ({match_type}) "
                                    f"{contact_display_name(main_contact)} +{tag_to_add}"
                                )
                        except Exception as exc:
                            print(f"    [WARN] Error añadiendo tag marca '{tag_to_add}': {exc}")
                if clean_mix and decision["tags_to_remove"]:
                    for tag_to_remove in decision["tags_to_remove"]:
                        try:
                            if safe_remove_contact_tag(
                                run_id, main_account["location_id"], main_contact,
                                tag_to_remove, main_account["token"], dry_run,
                            ):
                                print(
                                    f"  TAG- marca principal ({match_type}) "
                                    f"{contact_display_name(main_contact)} -{tag_to_remove}"
                                )
                        except Exception as exc:
                            print(f"    [WARN] Error removiendo tag marca '{tag_to_remove}': {exc}")

                main_changed = safe_update_contact_field(
                    run_id,
                    main_account["location_id"],
                    main_contact,
                    main_contact_schema["Canal de Origen"],
                    "Canal de Origen",
                    canal_value,
                    main_account["token"],
                    dry_run,
                )
                if main_changed:
                    print(f"  OK marca principal ({match_type}) {contact_display_name(main_contact)} -> Canal de Origen: {canal_value}")

        if contact_only:
            continue

        opps_for_contact = get_all_opportunities_for_contact(location_id, contact_id, token)
        if not opps_for_contact:
            skipped_no_opportunity += 1
        else:
            for opp_summary in opps_for_contact:
                opportunity_id = opp_summary.get("id")
                if not opportunity_id:
                    continue
                opportunity = get_opportunity(location_id, opportunity_id, token) or opp_summary
                opp_changed = False
                for field_name, value in TAG_TO_OPP_UPDATES[matched_tag].items():
                    # Resolver devuelve TODOS los IDs que matchean (maneja duplicados
                    # intra-location y aliases tipo "CANAL DE ORIGEN").
                    for field_id in resolve_opp_field_ids(opp_fields_list, field_name):
                        if safe_update_opportunity_field(run_id, location_id, opportunity_id, opportunity, field_id, field_name, value, token, dry_run):
                            opp_changed = True
                if opp_changed:
                    updated_opportunities += 1

        if main_contact and main_opp_fields_list:
            main_opps_for_contact = get_all_opportunities_for_contact(
                main_account["location_id"], main_contact["id"], main_account["token"]
            )
            for opp_summary in main_opps_for_contact:
                main_opportunity_id = opp_summary.get("id")
                if not main_opportunity_id:
                    continue
                main_opportunity = get_opportunity(
                    main_account["location_id"], main_opportunity_id, main_account["token"]
                ) or opp_summary
                main_opp_changed = False
                for field_name, value in TAG_TO_OPP_UPDATES[matched_tag].items():
                    for field_id in resolve_opp_field_ids(main_opp_fields_list, field_name):
                        if safe_update_opportunity_field(
                            run_id,
                            main_account["location_id"],
                            main_opportunity_id,
                            main_opportunity,
                            field_id,
                            field_name,
                            value,
                            main_account["token"],
                            dry_run,
                        ):
                            main_opp_changed = True
                if main_opp_changed:
                    print(f"  OK marca principal oportunidad {main_opportunity_id}")

    if contact_only:
        print(
            f"\n{name}: {updated_contacts} contactos actualizados, "
            f"{tag_changes} cambios de tag, {review_needed} en revisión"
        )
    else:
        print(
            f"\n{name}: {updated_contacts} contactos, {tag_changes} cambios de tag, "
            f"{updated_opportunities} oportunidades, {skipped_no_opportunity} sin oportunidad, "
            f"{review_needed} en revisión"
        )
    return updated_contacts


def main():
    parser = argparse.ArgumentParser(description="GHL Step 1: Tag -> Canal de Origen")
    parser.add_argument("--location", help="Specific location ID to process")
    parser.add_argument("--all", action="store_true", help="Process all branch locations from the CSV")
    parser.add_argument("--include-main", action="store_true", help="Include the main brand account when using --all")
    parser.add_argument("--dry-run", action="store_true", help="Preview changes without writing to GHL")
    parser.add_argument("--contact-only", action="store_true", help="Only update contact fields, skip opportunity fields")
    parser.add_argument("--sync-main", action="store_true", help="Also sync exact matching contact/opportunity in the main brand account")
    parser.add_argument("--keep-mix-tags", action="store_true", help="No remover tags conflictivos en mix (solo loguear)")
    parser.add_argument("--no-source-inference", action="store_true", help="No inferir/añadir tag a partir de source o canal previo")
    parser.add_argument("--run-id", help="Audit run ID supplied by the dashboard")
    args = parser.parse_args()

    print("\n" + "=" * 60)
    print("GHL WORKFLOW STEP 1 - TAG -> CANAL DE ORIGEN")
    print("=" * 60)
    if args.dry_run:
        print("DRY RUN - no changes will be made\n")

    total = 0
    for account in select_locations(args):
        try:
            total += process_location(
                account,
                dry_run=args.dry_run,
                contact_only=args.contact_only,
                run_id=args.run_id,
                sync_main=args.sync_main,
                clean_mix=not args.keep_mix_tags,
                infer_from_source=not args.no_source_inference,
            )
        except Exception as exc:
            print(f"\nERROR {account['nombre']}: {exc}")

    print(f"\n{'=' * 60}")
    print(f"TOTAL: {total} contactos evaluados/actualizados por tag")


if __name__ == "__main__":
    main()
