#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Compare GHL pipelines and stages across MP locations."""

import argparse
from collections import Counter
from datetime import datetime, timezone

from common import BRAND_LOCATION_ID, normalize_name, select_accounts
import db
from ghl_client import GHLClient


def clean_label(value):
    return " ".join(str(value or "").strip().split())


def parse_datetime(value):
    if not value:
        return None
    text = str(value).strip()
    try:
        if text.endswith("Z"):
            text = text[:-1] + "+00:00"
        return datetime.fromisoformat(text).astimezone(timezone.utc)
    except ValueError:
        return None


def display_date(value):
    parsed = parse_datetime(value)
    if not parsed:
        return value or "Sin fecha"
    return parsed.strftime("%Y-%m-%d %H:%M UTC")


def stage_name(stage):
    return clean_label(stage.get("name") or stage.get("title") or stage.get("id") or "Sin nombre")


def stage_order(stage, index):
    value = stage.get("order")
    if value is None:
        value = stage.get("position")
    try:
        return int(value)
    except (TypeError, ValueError):
        return index


def normalize_stage(stage, index):
    name = stage_name(stage)
    return {
        "id": stage.get("id"),
        "name": name,
        "norm_name": normalize_name(name),
        "order": stage_order(stage, index),
    }


def normalize_pipeline(pipeline):
    name = clean_label(pipeline.get("name") or pipeline.get("id") or "Sin nombre")
    raw_stages = pipeline.get("stages") or []
    stages = [normalize_stage(stage, idx) for idx, stage in enumerate(raw_stages)]
    stages.sort(key=lambda item: (item["order"], item["norm_name"]))
    return {
        "id": pipeline.get("id"),
        "name": name,
        "norm_name": normalize_name(name),
        "stages": stages,
        "date_added": pipeline.get("date_added") or pipeline.get("dateAdded") or pipeline.get("createdAt"),
        "date_updated": pipeline.get("date_updated") or pipeline.get("dateUpdated") or pipeline.get("updatedAt"),
    }


def pipeline_signature(pipelines):
    items = []
    for pipeline in sorted(pipelines, key=lambda item: item["norm_name"]):
        items.append((pipeline["norm_name"], tuple(stage["norm_name"] for stage in pipeline["stages"])))
    return tuple(items)


def build_location_report_live(account, client):
    location_id = account["location_id"]
    pipelines = client.get_pipelines(account["token"], location_id)
    normalized = [normalize_pipeline(pipeline) for pipeline in pipelines]
    return {
        "account": account,
        "pipelines": normalized,
        "signature": pipeline_signature(normalized),
        "error": None,
    }


def build_location_report_sqlite(account):
    location_id = account["location_id"]
    pipelines = db.get_pipelines(location_id)
    normalized = [normalize_pipeline(pipeline) for pipeline in pipelines]
    synthetic = any((pipeline.get("name") or "").lower().startswith("pipeline sintético") for pipeline in pipelines)
    return {
        "account": account,
        "pipelines": normalized,
        "signature": pipeline_signature(normalized),
        "error": None,
        "synthetic": synthetic,
    }


def indexed_by_normalized_name(items):
    result = {}
    duplicates = []
    for item in items:
        key = item["norm_name"]
        if key in result:
            duplicates.append(item["name"])
            continue
        result[key] = item
    return result, duplicates


def group_by_normalized_name(items):
    groups = {}
    for item in items:
        groups.setdefault(item["norm_name"], []).append(item)
    return groups


def pipeline_sort_date(pipeline):
    return parse_datetime(pipeline.get("date_added")) or parse_datetime(pipeline.get("date_updated")) or datetime.min.replace(tzinfo=timezone.utc)


def newest_pipeline(pipelines):
    return max(pipelines, key=pipeline_sort_date)


def duplicate_pipeline_groups(report):
    return {name: group for name, group in group_by_normalized_name(report["pipelines"]).items() if len(group) > 1}


def stage_map_by_position(old_pipeline, new_pipeline):
    old_stages = sorted(old_pipeline["stages"], key=lambda item: (item["order"], item["norm_name"]))
    new_stages = sorted(new_pipeline["stages"], key=lambda item: (item["order"], item["norm_name"]))
    if len(old_stages) != len(new_stages):
        return None
    return list(zip(old_stages, new_stages))


def build_duplicate_migration_plans(report):
    account = report["account"]
    plans = []
    for group in duplicate_pipeline_groups(report).values():
        official = newest_pipeline(group)
        obsolete_pipelines = [pipeline for pipeline in group if pipeline["id"] != official["id"]]
        for obsolete in obsolete_pipelines:
            mapping = stage_map_by_position(obsolete, official)
            opportunities = db.get_opportunities(account["location_id"], pipeline_id=obsolete["id"])
            plans.append({
                "account": account,
                "obsolete": obsolete,
                "official": official,
                "mapping": mapping,
                "opportunities": opportunities,
                "can_migrate": mapping is not None,
            })
    return plans


def compare_stages(reference_pipeline, actual_pipeline):
    issues = []
    reference_stages = reference_pipeline["stages"]
    actual_stages = actual_pipeline["stages"]
    reference_by_name, reference_duplicates = indexed_by_normalized_name(reference_stages)
    actual_by_name, actual_duplicates = indexed_by_normalized_name(actual_stages)

    if reference_duplicates:
        issues.append("Etapas duplicadas en referencia: " + ", ".join(reference_duplicates))
    if actual_duplicates:
        issues.append("Etapas duplicadas: " + ", ".join(actual_duplicates))

    missing = [stage["name"] for key, stage in reference_by_name.items() if key not in actual_by_name]
    extra = [stage["name"] for key, stage in actual_by_name.items() if key not in reference_by_name]

    if len(reference_stages) != len(actual_stages):
        issues.append(f"Cantidad de etapas distinta: esperado {len(reference_stages)}, actual {len(actual_stages)}")
    if missing:
        issues.append("Etapas faltantes: " + ", ".join(missing))
    if extra:
        issues.append("Etapas extra: " + ", ".join(extra))

    common_names = set(reference_by_name) & set(actual_by_name)
    for name in sorted(common_names):
        reference_name = reference_by_name[name]["name"]
        actual_name = actual_by_name[name]["name"]
        if reference_name != actual_name:
            issues.append(f"Nombre de etapa con escritura distinta: '{reference_name}' vs '{actual_name}'")

    reference_order = [stage["norm_name"] for stage in reference_stages if stage["norm_name"] in common_names]
    actual_order = [stage["norm_name"] for stage in actual_stages if stage["norm_name"] in common_names]
    if reference_order != actual_order:
        moved = []
        for name in common_names:
            ref_index = reference_order.index(name) + 1
            actual_index = actual_order.index(name) + 1
            if ref_index != actual_index:
                moved.append(f"{reference_by_name[name]['name']} ({ref_index} -> {actual_index})")
        if moved:
            issues.append("Orden de etapas distinto: " + ", ".join(sorted(moved)))

    return issues


def compare_to_reference(reference_report, actual_report):
    reference_pipelines = reference_report["pipelines"]
    actual_pipelines = actual_report["pipelines"]
    reference_by_name, reference_duplicates = indexed_by_normalized_name(reference_pipelines)
    actual_by_name, actual_duplicates = indexed_by_normalized_name(actual_pipelines)
    issues = []

    if reference_duplicates:
        issues.append("Pipelines duplicados en referencia: " + ", ".join(reference_duplicates))
    if actual_duplicates:
        issues.append("Pipelines duplicados: " + ", ".join(actual_duplicates))
    for group in duplicate_pipeline_groups(actual_report).values():
        official = newest_pipeline(group)
        obsolete = [pipeline for pipeline in group if pipeline["id"] != official["id"]]
        issues.append(
            f"Pipeline recomendado por fecha: {official['name']} ({official['id']}) | creado {display_date(official.get('date_added'))} | actualizado {display_date(official.get('date_updated'))}"
        )
        for pipeline in obsolete:
            issues.append(
                f"Pipeline obsoleto candidato: {pipeline['name']} ({pipeline['id']}) | creado {display_date(pipeline.get('date_added'))} | actualizado {display_date(pipeline.get('date_updated'))}"
            )

    missing = [pipeline["name"] for key, pipeline in reference_by_name.items() if key not in actual_by_name]
    extra = [pipeline["name"] for key, pipeline in actual_by_name.items() if key not in reference_by_name]

    if len(reference_pipelines) != len(actual_pipelines):
        issues.append(f"Cantidad de pipelines distinta: esperado {len(reference_pipelines)}, actual {len(actual_pipelines)}")
        reference_pipeline_names = ", ".join(pipeline["name"] for pipeline in reference_pipelines) or "Sin pipelines"
        actual_pipeline_names = ", ".join(
            f"{pipeline['name']} ({pipeline['id']})" if pipeline.get("id") else pipeline["name"]
            for pipeline in actual_pipelines
        ) or "Sin pipelines"
        issues.append(f"Pipelines en referencia: {reference_pipeline_names}")
        issues.append(f"Pipelines en cuenta evaluada: {actual_pipeline_names}")
    if total_stages(reference_report) != total_stages(actual_report):
        issues.append(f"Cantidad total de etapas distinta: esperado {total_stages(reference_report)}, actual {total_stages(actual_report)}")
    if missing:
        issues.append("Pipelines faltantes: " + ", ".join(missing))
    if extra:
        issues.append("Pipelines extra: " + ", ".join(extra))

    for key in sorted(set(reference_by_name) & set(actual_by_name)):
        if reference_by_name[key]["name"] != actual_by_name[key]["name"]:
            issues.append(
                f"Nombre de pipeline con escritura distinta: '{reference_by_name[key]['name']}' vs '{actual_by_name[key]['name']}'"
            )
        stage_issues = compare_stages(reference_by_name[key], actual_by_name[key])
        for issue in stage_issues:
            issues.append(f"Pipeline '{reference_by_name[key]['name']}': {issue}")

    return issues


def total_stages(report):
    return sum(len(pipeline["stages"]) for pipeline in report["pipelines"])


def print_pipeline_detail(report, show_ids):
    account = report["account"]
    print(f"\n{account['nombre']} ({account['location_id']})")
    print(f"  Pipelines: {len(report['pipelines'])} | Etapas totales: {total_stages(report)}")
    for pipeline in sorted(report["pipelines"], key=lambda item: item["norm_name"]):
        pipeline_id = f" | ID: {pipeline['id']}" if show_ids else ""
        print(
            f"  - Pipeline: {pipeline['name']} | Etapas: {len(pipeline['stages'])}"
            f" | Creado: {display_date(pipeline.get('date_added'))}"
            f" | Actualizado: {display_date(pipeline.get('date_updated'))}{pipeline_id}"
        )
        for idx, stage in enumerate(pipeline["stages"], 1):
            stage_id = f" | ID: {stage['id']}" if show_ids else ""
            print(f"      {idx}. {stage['name']}{stage_id}")


def issue_category(issue):
    if "Cantidad de pipelines" in issue:
        return "Cantidad de pipelines"
    if "Pipelines en referencia" in issue or "Pipelines en cuenta evaluada" in issue:
        return "Detalle de pipelines"
    if "Pipeline recomendado" in issue:
        return "Pipeline recomendado"
    if "Pipeline obsoleto" in issue:
        return "Pipeline obsoleto"
    if "Cantidad total de etapas" in issue:
        return "Cantidad total de etapas"
    if "Pipelines faltantes" in issue:
        return "Pipelines faltantes"
    if "Pipelines extra" in issue:
        return "Pipelines extra"
    if "Cantidad de etapas" in issue:
        return "Cantidad de etapas"
    if "Etapas faltantes" in issue:
        return "Etapas faltantes"
    if "Etapas extra" in issue:
        return "Etapas extra"
    if "Orden de etapas" in issue:
        return "Orden de etapas"
    if "escritura distinta" in issue:
        return "Nombres/escritura"
    if "duplicad" in issue.lower():
        return "Duplicados"
    return "Otros"


def print_findings_overview(comparisons):
    affected = [item for item in comparisons if item["status"] in ("DISCREPANCIA", "ERROR")]
    discrepancy_items = [item for item in comparisons if item["status"] == "DISCREPANCIA"]
    error_items = [item for item in comparisons if item["status"] == "ERROR"]
    total_issues = sum(len(item["issues"]) for item in affected)
    category_counts = Counter()

    for item in affected:
        for issue in item["issues"]:
            category_counts[issue_category(issue)] += 1

    print("\nOVERVIEW DE HALLAZGOS RELEVANTES")
    print("-" * 100)
    print(f"Cuentas afectadas: {len(affected)}")
    print(f"Cuentas con discrepancias: {len(discrepancy_items)}")
    print(f"Cuentas con error: {len(error_items)}")
    print(f"Hallazgos totales: {total_issues}")

    if not affected:
        print("Resultado: todas las cuentas coinciden con la referencia seleccionada.")
        return

    if category_counts:
        print("\nResumen por tipo de incongruencia:")
        for category, count in category_counts.most_common():
            print(f"  - {category}: {count}")

    print("\nCuentas afectadas y resumen:")
    for item in affected:
        report = item["report"]
        account = report["account"]
        issues = item["issues"]
        per_account_categories = Counter(issue_category(issue) for issue in issues)
        categories = ", ".join(f"{category} ({count})" for category, count in per_account_categories.most_common())
        print(f"  - {account['nombre']} ({account['location_id']}) [{item['status']}]: {len(issues)} hallazgo(s)")
        print(f"    Tipos: {categories}")
        for issue in issues[:5]:
            print(f"    * {issue}")
        if len(issues) > 5:
            print(f"    * ... y {len(issues) - 5} hallazgo(s) adicional(es).")


def print_migration_plan(plan):
    obsolete = plan["obsolete"]
    official = plan["official"]
    print(f"\nLocation: {plan['account']['nombre']} ({plan['account']['location_id']})")
    print(f"Pipeline obsoleto: {obsolete['name']} ({obsolete['id']}) | creado {display_date(obsolete.get('date_added'))} | actualizado {display_date(obsolete.get('date_updated'))}")
    print(f"Pipeline oficial: {official['name']} ({official['id']}) | creado {display_date(official.get('date_added'))} | actualizado {display_date(official.get('date_updated'))}")
    print(f"Etapas: {len(obsolete['stages'])} -> {len(official['stages'])} {'OK' if plan['can_migrate'] else 'NO MIGRABLE'}")
    print(f"Oportunidades en pipeline obsoleto: {len(plan['opportunities'])}")
    if not plan["can_migrate"]:
        print("Motivo: la cantidad de etapas entre pipeline obsoleto y oficial no coincide.")
        return
    print("Mapeo por posicion:")
    for idx, (old_stage, new_stage) in enumerate(plan["mapping"], 1):
        print(f"  {idx}. {old_stage['name']} ({old_stage['id']}) -> {new_stage['name']} ({new_stage['id']})")


def run_duplicate_pipeline_migration(comparisons, apply_changes, location_id, max_opps):
    print("\nMIGRACION DE PIPELINES DUPLICADOS")
    print("-" * 100)
    if apply_changes and not location_id:
        raise SystemExit("Error: --apply requiere --location para evitar migraciones globales accidentales.")

    plans = []
    for item in comparisons:
        report = item["report"]
        if report.get("error"):
            continue
        if report["account"]["location_id"] == BRAND_LOCATION_ID:
            continue
        plans.extend(build_duplicate_migration_plans(report))

    if not plans:
        print("No se encontraron pipelines duplicados candidatos para migracion.")
        return

    print("Modo: " + ("APPLY - se actualizaran oportunidades en GHL" if apply_changes else "DRY-RUN - no se modificara GHL"))
    client = GHLClient() if apply_changes else None
    total_candidates = 0
    total_updated = 0
    total_failed = 0

    for plan in plans:
        print_migration_plan(plan)
        if not plan["can_migrate"]:
            continue

        stage_id_map = {old_stage["id"]: new_stage["id"] for old_stage, new_stage in plan["mapping"]}
        candidates = []
        for opp in plan["opportunities"]:
            new_stage_id = stage_id_map.get(opp.get("pipeline_stage_id"))
            if new_stage_id:
                candidates.append((opp, new_stage_id))

        if max_opps is not None:
            candidates = candidates[:max_opps]
        total_candidates += len(candidates)
        print(f"Oportunidades migrables por etapa: {len(candidates)}")

        if not apply_changes:
            for opp, new_stage_id in candidates[:10]:
                print(f"  DRY-RUN: {opp['id']} | {opp.get('name') or 'Sin nombre'} -> pipeline {plan['official']['id']} / etapa {new_stage_id}")
            if len(candidates) > 10:
                print(f"  ... y {len(candidates) - 10} oportunidad(es) adicional(es).")
            continue

        token = plan["account"].get("token")
        for opp, new_stage_id in candidates:
            try:
                client.update_opportunity(token, opp["id"], {
                    "pipelineId": plan["official"]["id"],
                    "pipelineStageId": new_stage_id,
                })
                total_updated += 1
                print(f"  OK: {opp['id']} -> {plan['official']['id']} / {new_stage_id}")
            except Exception as exc:
                total_failed += 1
                print(f"  ERROR: {opp['id']} -> {exc}")

    print("\nResumen migracion")
    print(f"Planes evaluados: {len(plans)}")
    print(f"Oportunidades candidatas: {total_candidates}")
    if apply_changes:
        print(f"Oportunidades actualizadas: {total_updated}")
        print(f"Oportunidades fallidas: {total_failed}")
        print("Recomendacion: ejecuta Sincronizar Todo para refrescar SQLite despues del apply.")


def choose_main_reference(reports):
    for report in reports:
        if report["account"]["location_id"] == BRAND_LOCATION_ID:
            return report
    raise SystemExit("No se encontro la cuenta principal en el CSV de tokens.")


def choose_common_reference(reports):
    signatures = Counter(report["signature"] for report in reports)
    if not signatures:
        raise SystemExit("No hay cuentas para comparar.")
    signature, count = signatures.most_common(1)[0]
    for report in reports:
        if report["signature"] == signature:
            return report, count
    raise SystemExit("No se pudo resolver el patron comun.")


def parse_args():
    parser = argparse.ArgumentParser(
        description="Compara pipelines y etapas entre cuentas GHL de MP sin modificar datos."
    )
    mode = parser.add_mutually_exclusive_group()
    mode.add_argument("--reference-main", action="store_true", help="Usa la cuenta principal como referencia oficial")
    mode.add_argument("--reference-common", action="store_true", help="Usa el patron mas frecuente como referencia")
    parser.add_argument("--all", action="store_true", help="Compatibilidad: compara todas las cuentas desde SQLite")
    parser.add_argument("--location", help="Compara solo una location contra la referencia")
    parser.add_argument("--live", action="store_true", help="Consulta GHL en vivo en vez de usar SQLite local")
    parser.add_argument("--show-ids", action="store_true", help="Muestra IDs de pipelines y etapas")
    parser.add_argument("--details", action="store_true", help="Muestra el detalle completo de pipelines y etapas por cuenta")
    parser.add_argument("--migrate-duplicate-pipelines", action="store_true", help="Propone o aplica migracion de oportunidades desde pipelines duplicados antiguos al mas reciente")
    parser.add_argument("--apply", action="store_true", help="Aplica la migracion en GHL. Sin este flag solo hace dry-run")
    parser.add_argument("--max-opps", type=int, help="Limita oportunidades a migrar por plan")
    return parser.parse_args()


def main():
    args = parse_args()
    use_common_reference = args.reference_common
    accounts = select_accounts(location_id=args.location, all_locations=True, include_main=True)

    if args.location:
        main_accounts = [account for account in select_accounts(all_locations=True, include_main=True) if account["location_id"] == BRAND_LOCATION_ID]
        if main_accounts and all(account["location_id"] != BRAND_LOCATION_ID for account in accounts):
            accounts = main_accounts + accounts
    if args.apply and not args.migrate_duplicate_pipelines:
        raise SystemExit("Error: --apply solo es valido junto con --migrate-duplicate-pipelines.")

    print("=== COMPARATIVA DE PIPELINES Y ETAPAS GHL ===")
    print("Modo: " + ("patron comun" if use_common_reference else "cuenta principal"))
    print(f"Cuentas evaluadas: {len(accounts)}")
    print("Fuente: " + ("live API GHL, solo lectura" if args.live else "SQLite local precargado por Sincronizar Todo"))
    print("-" * 100)

    client = GHLClient() if args.live else None
    reports = []
    for index, account in enumerate(accounts, 1):
        action = "Consultando" if args.live else "Leyendo SQLite"
        print(f"[{index}/{len(accounts)}] {action} {account['nombre']} ({account['location_id']})...")
        try:
            if args.live:
                reports.append(build_location_report_live(account, client))
            else:
                reports.append(build_location_report_sqlite(account))
        except Exception as exc:
            reports.append({"account": account, "pipelines": [], "signature": tuple(), "error": str(exc)})
            print(f"  ERROR: {exc}")

    valid_reports = [report for report in reports if not report.get("error")]
    if not valid_reports:
        raise SystemExit("No se pudo consultar ninguna cuenta.")

    common_count = None
    if use_common_reference:
        reference_report, common_count = choose_common_reference(valid_reports)
    else:
        reference_report = choose_main_reference(valid_reports)

    reference_account = reference_report["account"]
    print("\nREFERENCIA")
    print(f"  {reference_account['nombre']} ({reference_account['location_id']})")
    print(f"  Pipelines: {len(reference_report['pipelines'])} | Etapas totales: {total_stages(reference_report)}")
    if not reference_report["pipelines"]:
        print("  Advertencia: la referencia no tiene pipelines en la fuente seleccionada. Ejecuta Sincronizar Todo o revisa permisos/API.")
    if reference_report.get("synthetic"):
        print("  Advertencia: la referencia contiene pipelines sinteticos generados desde oportunidades.")
    if common_count is not None:
        print(f"  Patron encontrado en {common_count} de {len(valid_reports)} cuentas consultadas correctamente")

    comparisons = []
    for report in reports:
        if report.get("error"):
            comparisons.append({"report": report, "status": "ERROR", "issues": [report["error"]]})
            continue
        issues = compare_to_reference(reference_report, report)
        is_reference = report["account"]["location_id"] == reference_account["location_id"]
        status = "REFERENCIA" if is_reference else ("OK" if not issues else "DISCREPANCIA")
        comparisons.append({"report": report, "status": status, "issues": issues})

    print("\nRESUMEN CONSOLIDADO")
    print(f"{'CUENTA':<36} | {'PIPELINES':<9} | {'ETAPAS':<7} | {'ESTADO':<13}")
    print("-" * 100)
    ok_count = discrepancy_count = error_count = 0
    for item in comparisons:
        report = item["report"]
        account = report["account"]
        status = item["status"]
        if status == "OK" or status == "REFERENCIA":
            ok_count += 1
        elif status == "ERROR":
            error_count += 1
        else:
            discrepancy_count += 1
        synthetic_mark = " *" if report.get("synthetic") else ""
        print(f"{account['nombre'][:36]:<36} | {len(report['pipelines']):<9} | {total_stages(report):<7} | {(status + synthetic_mark):<13}")

    print("-" * 100)
    print(f"Cuentas OK/referencia: {ok_count}")
    print(f"Cuentas con discrepancias: {discrepancy_count}")
    print(f"Cuentas con error: {error_count}")
    if any(item["report"].get("synthetic") for item in comparisons):
        print("Nota: * indica pipelines sinteticos creados por el sync cuando GHL no devolvio pipelines reales.")

    print_findings_overview(comparisons)

    if args.details:
        print("\nDETALLE DE PIPELINES Y ETAPAS")
        for report in reports:
            if not report.get("error"):
                print_pipeline_detail(report, args.show_ids)

    print("\nDISCREPANCIAS")
    any_discrepancy = False
    for item in comparisons:
        if item["status"] not in ("DISCREPANCIA", "ERROR"):
            continue
        any_discrepancy = True
        account = item["report"]["account"]
        print(f"\n- {account['nombre']} ({account['location_id']}) [{item['status']}]")
        for issue in item["issues"]:
            print(f"  * {issue}")
    if not any_discrepancy:
        print("No se encontraron discrepancias contra la referencia seleccionada.")

    if args.migrate_duplicate_pipelines:
        run_duplicate_pipeline_migration(comparisons, args.apply, args.location, args.max_opps)

    print("\nComparativa finalizada.")


if __name__ == "__main__":
    main()
