"""Guarded Magento site bootstrap/check.

This wraps the existing catalog provisioner with a conservative taxonomy sanity
check before creating categories or assigning SKUs on a new Magento connection.

CLI:
    python -m app.jobs.magento_site_bootstrap_check --connection-id 2
    python -m app.jobs.magento_site_bootstrap_check --connection-id 2 --apply
    python -m app.jobs.magento_site_bootstrap_check --connection-id 2 --apply --allow-risky
"""

from __future__ import annotations

import argparse
import json
import re
import sys
from datetime import datetime, timezone
from typing import Any, Dict, Iterable, List, Optional

from sqlalchemy import select
from sqlalchemy.orm import Session

from channel.url_canonical import normalize_plp_path
from db.models import ChannelListingPath, MagentoCategoryRegistry, ProductListingPathAssignment


BLOCKING_SEVERITIES = {"error"}

HUB_RULES = {
    "kitchen-cabinets": {
        "label": "Kitchen Cabinets",
        "expected": ("kitchen", "cabinet"),
        "conflicts": ("bathroom", "vanit"),
    },
    "bathroom-vanities": {
        "label": "Bathroom Vanities",
        "expected": ("bathroom", "vanit"),
        "conflicts": ("kitchen", "cabinet"),
    },
}


def _norm_text(value: Any) -> str:
    text = str(value or "").strip().lower()
    return re.sub(r"\s+", " ", text)


def _slug(value: Any) -> str:
    text = _norm_text(value)
    text = re.sub(r"[^a-z0-9]+", "-", text).strip("-")
    return text


def _leaf(path: str) -> str:
    parts = [part.strip() for part in str(path or "").split("/") if part.strip()]
    return parts[-1] if parts else ""


def _hub_for_path_slug(path_slug: str) -> Optional[str]:
    slug = normalize_plp_path(path_slug)
    first = slug.split("/", 1)[0] if slug else ""
    return first if first in HUB_RULES else None


def _text_has_any(text: str, needles: Iterable[str]) -> bool:
    return any(needle in text for needle in needles)


def _issue(
    severity: str,
    code: str,
    message: str,
    **details: Any,
) -> Dict[str, Any]:
    payload = {
        "severity": severity,
        "code": code,
        "message": message,
    }
    payload.update({k: v for k, v in details.items() if v is not None})
    return payload


def analyze_magento_site_bootstrap(
    session: Session,
    *,
    connection_id: int,
    provision_result: Dict[str, Any],
) -> Dict[str, Any]:
    """Return taxonomy/category diagnostics for a planned Magento provision run."""
    issues: List[Dict[str, Any]] = []
    planned_paths = _planned_magento_paths(provision_result)

    issues.extend(_check_backend_listing_paths(session))
    issues.extend(_check_planned_paths(planned_paths))
    issues.extend(_check_magento_registry(session, connection_id=connection_id, planned_paths=planned_paths))

    blocking = [item for item in issues if item.get("severity") in BLOCKING_SEVERITIES]
    warnings = [item for item in issues if item.get("severity") == "warning"]
    return {
        "status": "blocked" if blocking else "ok",
        "connection_id": connection_id,
        "planned_magento_category_count": len(planned_paths),
        "issue_count": len(issues),
        "blocking_issue_count": len(blocking),
        "warning_count": len(warnings),
        "issues": issues,
    }


def _planned_magento_paths(provision_result: Dict[str, Any]) -> List[str]:
    samples = ((provision_result.get("samples") or {}).get("magento_categories") or [])
    out: List[str] = []
    for item in samples:
        path = item.get("path") if isinstance(item, dict) else item
        if path:
            out.append(str(path))
    return sorted(set(out))


def _check_backend_listing_paths(session: Session) -> List[Dict[str, Any]]:
    issues: List[Dict[str, Any]] = []
    for row, rule in _conflicting_backend_listing_paths(session):
        issues.append(
            _issue(
                "error",
                "backend_listing_title_conflict",
                f"Backend listing path '{row.path_slug}' has a title that looks like another hub.",
                path_slug=row.path_slug,
                title=row.title,
                expected_hub=rule["label"],
            )
        )
    return issues


def _conflicting_backend_listing_paths(session: Session) -> List[tuple[ChannelListingPath, Dict[str, Any]]]:
    rows = session.scalars(select(ChannelListingPath).where(ChannelListingPath.is_active.is_(True))).all()
    conflicts: List[tuple[ChannelListingPath, Dict[str, Any]]] = []
    for row in rows:
        hub = _hub_for_path_slug(row.path_slug)
        if not hub:
            continue
        rule = HUB_RULES[hub]
        title = _norm_text(row.title)
        if _text_has_any(title, rule["conflicts"]):
            conflicts.append((row, rule))
    return conflicts


def deactivate_conflicting_backend_listing_paths(session: Session, *, dry_run: bool = True) -> Dict[str, Any]:
    """Deactivate active backend PLP rows that clearly belong under a different hub."""
    conflicts = _conflicting_backend_listing_paths(session)
    path_ids = [row.id for row, _ in conflicts]
    assignments = []
    if path_ids:
        assignments = session.scalars(
            select(ProductListingPathAssignment)
            .where(ProductListingPathAssignment.listing_path_id.in_(path_ids))
            .where(ProductListingPathAssignment.assignment_status == "active")
        ).all()

    planned_paths = [
        {
            "id": row.id,
            "path_slug": row.path_slug,
            "title": row.title,
            "expected_hub": rule["label"],
        }
        for row, rule in conflicts
    ]
    result = {
        "status": "planned" if dry_run else "applied",
        "dry_run": dry_run,
        "deactivated_listing_paths": len(conflicts) if not dry_run else 0,
        "deactivated_assignments": len(assignments) if not dry_run else 0,
        "planned_listing_paths": planned_paths,
        "planned_assignment_count": len(assignments),
    }
    if dry_run:
        return result

    now = datetime.now(timezone.utc).isoformat()
    note = f"deactivated by magento_site_bootstrap_check at {now}"
    for row, _ in conflicts:
        row.is_active = False
        row.notes = _append_note(row.notes, note)
    for assignment in assignments:
        assignment.assignment_status = "inactive"
        assignment.notes = _append_note(assignment.notes, note)
    session.flush()
    return result


def _append_note(existing: Optional[str], note: str) -> str:
    text = str(existing or "").strip()
    return f"{text}\n{note}" if text else note


def _check_planned_paths(planned_paths: List[str]) -> List[Dict[str, Any]]:
    issues: List[Dict[str, Any]] = []
    for path in planned_paths:
        slug = normalize_plp_path(path)
        hub = _hub_for_path_slug(slug)
        if not hub:
            continue
        rule = HUB_RULES[hub]
        text = _norm_text(path)
        if _text_has_any(text, rule["conflicts"]):
            issues.append(
                _issue(
                    "error",
                    "planned_category_path_conflict",
                    f"Planned Magento path '{path}' mixes taxonomy language from another hub.",
                    path=path,
                    expected_hub=rule["label"],
                )
            )
    return issues


def _check_magento_registry(
    session: Session,
    *,
    connection_id: int,
    planned_paths: List[str],
) -> List[Dict[str, Any]]:
    rows = session.scalars(
        select(MagentoCategoryRegistry).where(MagentoCategoryRegistry.connection_id == connection_id)
    ).all()
    rows_by_slug = {_slug(row.path_names or row.name): row for row in rows}
    rows_by_leaf_slug = {_slug(row.name): row for row in rows}
    issues: List[Dict[str, Any]] = []

    for row in rows:
        row_slug_path = normalize_plp_path(row.path_names or row.name)
        hub = _hub_for_path_slug(row_slug_path)
        if not hub:
            continue
        rule = HUB_RULES[hub]
        registry_text = _norm_text(f"{row.name} {row.path_names or ''}")
        if _text_has_any(registry_text, rule["conflicts"]):
            issues.append(
                _issue(
                    "error",
                    "magento_registry_hub_conflict",
                    "Existing Magento category registry contains conflicting hub language.",
                    expected_hub=rule["label"],
                    magento_category_id=row.category_id,
                    magento_name=row.name,
                    magento_path_names=row.path_names,
                )
            )

    for path in planned_paths:
        path_slug = _slug(path)
        leaf_slug = _slug(_leaf(path))
        row = rows_by_slug.get(path_slug) or rows_by_leaf_slug.get(leaf_slug)
        if row is None:
            continue

        expected_leaf = _leaf(path)
        if expected_leaf and row.name and _slug(row.name) != _slug(expected_leaf):
            issues.append(
                _issue(
                    "error",
                    "magento_category_leaf_mismatch",
                    "Existing Magento category matched the backend path but its title differs.",
                    path=path,
                    expected_title=expected_leaf,
                    magento_category_id=row.category_id,
                    magento_name=row.name,
                    magento_path_names=row.path_names,
                )
            )

        hub = _hub_for_path_slug(normalize_plp_path(path))
        if not hub:
            continue
        rule = HUB_RULES[hub]
        registry_text = _norm_text(f"{row.name} {row.path_names or ''}")
        if _text_has_any(registry_text, rule["conflicts"]):
            issues.append(
                _issue(
                    "error",
                    "magento_registry_hub_conflict",
                    "Existing Magento category registry contains conflicting hub language.",
                    path=path,
                    expected_hub=rule["label"],
                    magento_category_id=row.category_id,
                    magento_name=row.name,
                    magento_path_names=row.path_names,
                )
            )
    return issues


def run_magento_site_bootstrap_check(
    *,
    connection_id: int,
    apply: bool = False,
    allow_risky: bool = False,
    deactivate_conflicts: bool = False,
    all_products: bool = True,
    baseline_first: bool = True,
    limit: Optional[int] = None,
) -> Dict[str, Any]:
    from app.jobs.channel_catalog_provision import run_channel_catalog_provision
    from app.jobs.magento_baseline_sync import run_baseline_sync
    from db.session import get_session

    with get_session() as session:
        baseline_result = None
        if baseline_first:
            baseline_result = run_baseline_sync(connection_id, force=True, session=session)
            if baseline_result.get("status") == "failed":
                return {
                    "status": "failed",
                    "connection_id": connection_id,
                    "stage": "baseline",
                    "baseline": baseline_result,
                }

        preview = run_channel_catalog_provision(
            dry_run=True,
            magento_connection_id=connection_id,
            include_magento=True,
            include_shopify=False,
            only_assigned=not all_products,
            baseline_first=False,
            limit=limit,
        )
        full_plan_preview = _build_full_magento_plan_preview(
            session,
            connection_id=connection_id,
            only_assigned=not all_products,
            limit=limit,
        )
        diagnostics = analyze_magento_site_bootstrap(
            session,
            connection_id=connection_id,
            provision_result=full_plan_preview,
        )
        repair = None
        if diagnostics["status"] == "blocked" and deactivate_conflicts:
            repair = deactivate_conflicting_backend_listing_paths(session, dry_run=False)
            session.commit()
            diagnostics = analyze_magento_site_bootstrap(
                session,
                connection_id=connection_id,
                provision_result=full_plan_preview,
            )

        result: Dict[str, Any] = {
            "status": "blocked" if diagnostics["status"] == "blocked" and not allow_risky else "ok",
            "connection_id": connection_id,
            "apply_requested": apply,
            "allow_risky": allow_risky,
            "deactivate_conflicts": deactivate_conflicts,
            "baseline": baseline_result,
            "preview": preview,
            "diagnostics": diagnostics,
            "repair": repair,
            "apply": None,
        }
        if diagnostics["status"] == "blocked" and not allow_risky:
            return result
        if apply:
            result["apply"] = run_channel_catalog_provision(
                dry_run=False,
                magento_connection_id=connection_id,
                include_magento=True,
                include_shopify=False,
                only_assigned=not all_products,
                baseline_first=False,
                limit=limit,
            )
            result["status"] = "applied"
        return result


def _build_full_magento_plan_preview(
    session: Session,
    *,
    connection_id: int,
    only_assigned: bool,
    limit: Optional[int],
) -> Dict[str, Any]:
    from db.channel_catalog_provision import (
        ProvisionOptions,
        _load_products,
        build_channel_catalog_plan,
    )

    options = ProvisionOptions(
        magento_connection_id=connection_id,
        shopify_connection_id=None,
        only_assigned=only_assigned,
        dry_run=True,
        include_magento=True,
        include_shopify=False,
        limit=limit,
    )
    products = _load_products(session, options=options)
    plan = build_channel_catalog_plan(session, products, options=options)
    return {
        "samples": {
            "magento_categories": plan.get("magento", {}).get("category_paths", []),
        }
    }


def main() -> int:
    parser = argparse.ArgumentParser(description="Guarded Magento site bootstrap/check")
    parser.add_argument("--connection-id", type=int, required=True)
    parser.add_argument("--apply", action="store_true", help="Apply provision after diagnostics pass")
    parser.add_argument("--allow-risky", action="store_true", help="Apply even when diagnostics are blocking")
    parser.add_argument(
        "--deactivate-conflicting-listing-paths",
        action="store_true",
        help="Deactivate backend listing paths that mix known hubs, then rerun diagnostics",
    )
    parser.add_argument("--assigned-only", action="store_true", help="Use only channel-assigned SKUs")
    parser.add_argument("--no-baseline", dest="baseline_first", action="store_false")
    parser.add_argument("--limit", type=int, default=None)
    args = parser.parse_args()

    result = run_magento_site_bootstrap_check(
        connection_id=args.connection_id,
        apply=args.apply,
        allow_risky=args.allow_risky,
        deactivate_conflicts=args.deactivate_conflicting_listing_paths,
        all_products=not args.assigned_only,
        baseline_first=args.baseline_first,
        limit=args.limit,
    )
    print(json.dumps(result, indent=2, default=str))
    return 1 if result.get("status") in {"failed", "blocked"} else 0


if __name__ == "__main__":
    sys.exit(main())
