"""Manual taxonomy/collection-scoped attribute lock rules."""

from __future__ import annotations

from typing import Any, Dict, List, Optional

from sqlalchemy import delete, select
from sqlalchemy.orm import Session

from channel.url_canonical import normalize_plp_path
from db.master_taxonomy_registry import alias_codes_from_json
from db.models import (
    ManualAttributeLockRule,
    ManualAttributeLockTarget,
    MasterCollectionRegistry,
)


def _normalize_values(values: Any) -> List[str]:
    if isinstance(values, str):
        values = [part.strip() for part in values.split(",")]
    if not isinstance(values, list):
        return []
    seen = set()
    out: List[str] = []
    for item in values:
        text = str(item or "").strip()
        if text and text not in seen:
            seen.add(text)
            out.append(text)
    return out


def _collection_lookup(session: Session) -> Dict[str, Dict[str, Any]]:
    lookup: Dict[str, Dict[str, Any]] = {}
    rows = session.scalars(
        select(MasterCollectionRegistry)
        .where(MasterCollectionRegistry.is_active.is_(True))
        .order_by(MasterCollectionRegistry.id)
    ).all()
    for row in rows:
        payload = {
            "id": row.id,
            "code": str(row.code or "").strip().upper(),
            "name": row.name,
            "path_slug": row.path_slug,
            "source_collection_code": str((row.aliases or {}).get("source_collection_code") or "").strip().upper()
            if isinstance(row.aliases, dict)
            else "",
            "alias_codes": alias_codes_from_json(row.aliases),
        }
        keys = {
            payload["code"],
            payload["source_collection_code"],
            *(str(code or "").strip().upper() for code in payload["alias_codes"]),
        }
        for key in keys:
            if key:
                lookup.setdefault(key, payload)
    return lookup


def _target_dict(row: ManualAttributeLockTarget) -> Dict[str, Any]:
    values = _normalize_values(row.attribute_values_json if isinstance(row.attribute_values_json, list) else [])
    if not values and str(row.attribute_value or "").strip():
        values = [str(row.attribute_value).strip()]
    mode = str(row.assignment_mode or "").strip() or ("allow_list" if len(values) > 1 else "force")
    return {
        "attribute_code": row.attribute_code,
        "assignment_mode": mode,
        "attribute_value": str(row.attribute_value or "").strip() or None,
        "attribute_values": values,
        "is_locked": bool(row.is_locked),
    }


def _rule_dict(rule: ManualAttributeLockRule, targets: List[ManualAttributeLockTarget]) -> Dict[str, Any]:
    return {
        "canonical_taxonomy_path_slug": str(rule.canonical_taxonomy_path_slug or "").strip(),
        "path_match_kind": str(rule.path_match_kind or "exact").strip() or "exact",
        "collection_registry_id": rule.collection_registry_id,
        "collection_code": str(rule.collection_code or "").strip().upper() or None,
        "collection_name": str(rule.collection_name or "").strip() or None,
        "priority": int(rule.priority or 100),
        "rule_status": str(rule.rule_status or "active").strip() or "active",
        "targets": [_target_dict(target) for target in sorted(targets, key=lambda item: (item.sort_order, item.id))],
    }


def list_manual_attribute_locks(session: Session) -> Dict[str, Any]:
    rules = session.scalars(
        select(ManualAttributeLockRule)
        .where(ManualAttributeLockRule.rule_status == "active")
        .order_by(
            ManualAttributeLockRule.canonical_taxonomy_path_slug,
            ManualAttributeLockRule.collection_code,
            ManualAttributeLockRule.priority,
            ManualAttributeLockRule.id,
        )
    ).all()
    if not rules:
        return {"templates": [], "rules": []}
    rule_ids = [row.id for row in rules]
    targets = session.scalars(
        select(ManualAttributeLockTarget)
        .where(ManualAttributeLockTarget.rule_id.in_(rule_ids))
        .order_by(ManualAttributeLockTarget.rule_id, ManualAttributeLockTarget.sort_order, ManualAttributeLockTarget.id)
    ).all()
    targets_by_rule: Dict[int, List[ManualAttributeLockTarget]] = {}
    for target in targets:
        targets_by_rule.setdefault(target.rule_id, []).append(target)
    templates = [_rule_dict(rule, targets_by_rule.get(rule.id, [])) for rule in rules]
    flat_rules = []
    for item in templates:
        for target in item["targets"]:
            flat_rules.append(
                {
                    "canonical_taxonomy_path_slug": item["canonical_taxonomy_path_slug"],
                    "path_match_kind": item["path_match_kind"],
                    "collection_code": item["collection_code"],
                    "collection_name": item["collection_name"],
                    "attribute_code": target["attribute_code"],
                    "assignment_mode": target["assignment_mode"],
                    "attribute_value": target["attribute_value"],
                    "attribute_values": target["attribute_values"],
                }
            )
    return {"templates": templates, "rules": flat_rules}


def replace_manual_attribute_locks(session: Session, payload: Dict[str, Any]) -> Dict[str, Any]:
    templates = payload.get("templates")
    if not isinstance(templates, list):
        raise ValueError("templates must be a list")
    lookup = _collection_lookup(session)
    saved_rules = 0
    saved_targets = 0

    for item in templates:
        path_slug = normalize_plp_path(item.get("canonical_taxonomy_path_slug") or "")
        if not path_slug:
            continue
        path_match_kind = str(item.get("path_match_kind") or "exact").strip().lower() or "exact"
        collection_code = str(item.get("collection_code") or "").strip().upper() or None
        collection_row = lookup.get(collection_code) if collection_code else None
        if collection_code and collection_row is None:
            collection_code = None

        stmt = select(ManualAttributeLockRule).where(
            ManualAttributeLockRule.canonical_taxonomy_path_slug == path_slug,
            ManualAttributeLockRule.path_match_kind == path_match_kind,
        )
        if collection_code is None:
            stmt = stmt.where(ManualAttributeLockRule.collection_code.is_(None))
        else:
            stmt = stmt.where(ManualAttributeLockRule.collection_code == collection_code)
        rule = session.scalar(stmt)
        if rule is None:
            rule = ManualAttributeLockRule(
                canonical_taxonomy_path_slug=path_slug,
                path_match_kind=path_match_kind,
            )
            session.add(rule)
            session.flush()
        else:
            session.execute(delete(ManualAttributeLockTarget).where(ManualAttributeLockTarget.rule_id == rule.id))

        rule.collection_registry_id = collection_row.get("id") if collection_row else None
        rule.collection_code = collection_code
        rule.collection_name = (
            str(item.get("collection_name") or "").strip()
            or (str(collection_row.get("name") or "").strip() if collection_row else None)
        ) or None
        rule.priority = int(item.get("priority") or 100)
        rule.rule_status = str(item.get("rule_status") or "active").strip() or "active"
        rule.raw_payload = item
        saved_rules += 1

        seen_codes = set()
        for index, target in enumerate(item.get("targets") or []):
            code = str(target.get("attribute_code") or "").strip()
            if not code or code in seen_codes:
                continue
            seen_codes.add(code)
            values = _normalize_values(target.get("attribute_values"))
            single = str(target.get("attribute_value") or "").strip()
            if single and single not in values:
                values = [single, *values] if values else [single]
            assignment_mode = str(target.get("assignment_mode") or "").strip().lower()
            if assignment_mode not in {"force", "allow_list"}:
                assignment_mode = "allow_list" if len(values) > 1 else "force"
            if assignment_mode == "force" and values:
                single = values[0]
                values = [single]
            session.add(
                ManualAttributeLockTarget(
                    rule_id=rule.id,
                    attribute_code=code,
                    assignment_mode=assignment_mode,
                    attribute_value=single or (values[0] if len(values) == 1 else None),
                    attribute_values_json=values or None,
                    is_locked=bool(target.get("is_locked", True)),
                    sort_order=index,
                    raw_payload=target,
                )
            )
            saved_targets += 1

    session.flush()
    result = list_manual_attribute_locks(session)
    result["saved_rule_count"] = saved_rules
    result["saved_target_count"] = saved_targets
    return result


def resolve_manual_attribute_locks(
    session: Session,
    *,
    canonical_taxonomy_path_slug: Optional[str],
    collection_code: Optional[str] = None,
) -> Dict[str, Dict[str, Any]]:
    path_slug = normalize_plp_path(canonical_taxonomy_path_slug or "")
    normalized_collection_code = str(collection_code or "").strip().upper() or None
    if not path_slug:
        return {}

    rows = session.execute(
        select(ManualAttributeLockRule, ManualAttributeLockTarget)
        .join(
            ManualAttributeLockTarget,
            ManualAttributeLockTarget.rule_id == ManualAttributeLockRule.id,
        )
        .where(ManualAttributeLockRule.rule_status == "active")
        .where(ManualAttributeLockTarget.is_locked.is_(True))
        .order_by(
            ManualAttributeLockRule.priority.asc(),
            ManualAttributeLockRule.id.asc(),
            ManualAttributeLockTarget.sort_order.asc(),
            ManualAttributeLockTarget.id.asc(),
        )
    ).all()

    resolved: Dict[str, Dict[str, Any]] = {}
    winning_scores: Dict[str, tuple[int, int, int]] = {}
    for rule, target in rows:
        rule_path = normalize_plp_path(rule.canonical_taxonomy_path_slug or "")
        if not rule_path:
            continue
        match_kind = str(rule.path_match_kind or "exact").strip().lower() or "exact"
        path_match = rule_path == path_slug if match_kind == "exact" else (
            path_slug == rule_path or path_slug.startswith(f"{rule_path}/")
        )
        if not path_match:
            continue

        rule_collection_code = str(rule.collection_code or "").strip().upper() or None
        if rule_collection_code and normalized_collection_code != rule_collection_code:
            continue

        values = _normalize_values(target.attribute_values_json if isinstance(target.attribute_values_json, list) else [])
        if not values and str(target.attribute_value or "").strip():
            values = [str(target.attribute_value).strip()]
        if not values:
            continue
        collection_specificity = 1 if rule_collection_code else 0
        match_specificity = 1 if match_kind == "exact" else 0
        path_specificity = len([part for part in rule_path.split("/") if part])
        score = (collection_specificity, match_specificity, path_specificity)
        attr_code = str(target.attribute_code or "").strip()
        if not attr_code:
            continue
        current_score = winning_scores.get(attr_code)
        if current_score is not None and current_score >= score:
            continue
        winning_scores[attr_code] = score
        resolved[attr_code] = {
            "attribute_code": attr_code,
            "assignment_mode": str(target.assignment_mode or "").strip() or ("allow_list" if len(values) > 1 else "force"),
            "attribute_value": str(target.attribute_value or "").strip() or values[0],
            "attribute_values": values,
        }
    return resolved
