"""Rule and rule-group management, plus a rule tester."""

from __future__ import annotations

from fastapi import APIRouter, Depends, HTTPException, Query, status
from pydantic import ValidationError
from sqlalchemy import select
from sqlalchemy.orm import Session

from app.database import get_db
from app.models import Campaign, Lead, Rule, RuleGroup
from app.models.enums import RuleAction
from app.rules.engine import RuleSpec, match_rule
from app.rules.fields import RULE_FIELDS, lead_to_context
from app.rules.operators import OPERATOR_LABELS
from app.rules.scoring import band_summary
from app.schemas.base import Message
from app.schemas.rule import (
    RuleCreate,
    RuleGroupCreate,
    RuleGroupRead,
    RuleGroupUpdate,
    RuleMetadata,
    RuleRead,
    RuleTestRequest,
    RuleTestResponse,
    RuleUpdate,
)

router = APIRouter(prefix="/rules", tags=["rules"])


@router.get("/metadata", response_model=RuleMetadata)
def rule_metadata() -> RuleMetadata:
    """Everything the rule builder UI needs to render its dropdowns."""
    return RuleMetadata(
        fields=RULE_FIELDS,
        operators=[{"value": key, "label": label} for key, label in OPERATOR_LABELS.items()],
        actions=[action.value for action in RuleAction],
        score_bands=band_summary(),
    )


@router.get("", response_model=list[RuleRead])
def list_rules(
    db: Session = Depends(get_db),
    campaign_id: int | None = Query(default=None, alias="campaignId"),
    include_global: bool = Query(default=True, alias="includeGlobal"),
    only_active: bool = Query(default=False, alias="onlyActive"),
) -> list[RuleRead]:
    query = select(Rule)
    if campaign_id is not None:
        query = (
            query.where((Rule.campaign_id == campaign_id) | (Rule.campaign_id.is_(None)))
            if include_global
            else query.where(Rule.campaign_id == campaign_id)
        )
    if only_active:
        query = query.where(Rule.is_active.is_(True))
    query = query.order_by(Rule.priority, Rule.id)
    return [RuleRead.model_validate(rule) for rule in db.scalars(query).all()]


def _check_references(db: Session, campaign_id: int | None, group_id: int | None) -> None:
    """Turn a stale campaign/group id into a 404 instead of a database error."""
    if campaign_id is not None and db.get(Campaign, campaign_id) is None:
        raise HTTPException(status_code=404, detail=f"Campaign {campaign_id} not found")
    if group_id is not None and db.get(RuleGroup, group_id) is None:
        raise HTTPException(status_code=404, detail=f"Rule group {group_id} not found")


@router.post("", response_model=RuleRead, status_code=status.HTTP_201_CREATED)
def create_rule(payload: RuleCreate, db: Session = Depends(get_db)) -> RuleRead:
    _check_references(db, payload.campaign_id, payload.group_id)
    rule = Rule(**payload.model_dump())
    db.add(rule)
    db.commit()
    db.refresh(rule)
    return RuleRead.model_validate(rule)


@router.post("/test", response_model=RuleTestResponse)
def test_rule(payload: RuleTestRequest, db: Session = Depends(get_db)) -> RuleTestResponse:
    """Dry-run a rule against a saved lead or an inline sample prospect."""
    if payload.rule_id is not None:
        stored = db.get(Rule, payload.rule_id)
        if stored is None:
            raise HTTPException(status_code=404, detail=f"Rule {payload.rule_id} not found")
        spec = RuleSpec.from_orm(stored)
    elif payload.rule is not None:
        spec = RuleSpec.from_dict(payload.rule.model_dump())
    else:
        raise HTTPException(status_code=422, detail="Provide either rule or ruleId")

    if payload.lead_id is not None:
        lead = db.get(Lead, payload.lead_id)
        if lead is None:
            raise HTTPException(status_code=404, detail=f"Lead {payload.lead_id} not found")
        context = lead_to_context(lead)
    else:
        context = lead_to_context(payload.sample or {})

    matched, error = match_rule(spec, context)
    return RuleTestResponse(
        matched=matched,
        points=spec.points if matched and spec.action == RuleAction.SCORE else 0,
        action=spec.action,
        detail=spec.describe(),
        error=error,
    )


@router.get("/groups", response_model=list[RuleGroupRead])
def list_groups(
    db: Session = Depends(get_db),
    campaign_id: int | None = Query(default=None, alias="campaignId"),
) -> list[RuleGroupRead]:
    query = select(RuleGroup).order_by(RuleGroup.priority, RuleGroup.id)
    if campaign_id is not None:
        query = query.where(
            (RuleGroup.campaign_id == campaign_id) | (RuleGroup.campaign_id.is_(None))
        )
    return [RuleGroupRead.model_validate(group) for group in db.scalars(query).all()]


@router.post("/groups", response_model=RuleGroupRead, status_code=status.HTTP_201_CREATED)
def create_group(payload: RuleGroupCreate, db: Session = Depends(get_db)) -> RuleGroupRead:
    group = RuleGroup(**payload.model_dump())
    db.add(group)
    db.commit()
    db.refresh(group)
    return RuleGroupRead.model_validate(group)


@router.put("/groups/{group_id}", response_model=RuleGroupRead)
def update_group(
    group_id: int, payload: RuleGroupUpdate, db: Session = Depends(get_db)
) -> RuleGroupRead:
    group = db.get(RuleGroup, group_id)
    if group is None:
        raise HTTPException(status_code=404, detail=f"Rule group {group_id} not found")
    for key, value in payload.model_dump(exclude_unset=True).items():
        setattr(group, key, value)
    db.commit()
    db.refresh(group)
    return RuleGroupRead.model_validate(group)


@router.delete("/groups/{group_id}", response_model=Message)
def delete_group(group_id: int, db: Session = Depends(get_db)) -> Message:
    group = db.get(RuleGroup, group_id)
    if group is None:
        raise HTTPException(status_code=404, detail=f"Rule group {group_id} not found")
    db.delete(group)
    db.commit()
    return Message(detail=f"Deleted rule group '{group.name}'")


@router.get("/{rule_id}", response_model=RuleRead)
def get_rule(rule_id: int, db: Session = Depends(get_db)) -> RuleRead:
    rule = db.get(Rule, rule_id)
    if rule is None:
        raise HTTPException(status_code=404, detail=f"Rule {rule_id} not found")
    return RuleRead.model_validate(rule)


@router.put("/{rule_id}", response_model=RuleRead)
def update_rule(rule_id: int, payload: RuleUpdate, db: Session = Depends(get_db)) -> RuleRead:
    rule = db.get(Rule, rule_id)
    if rule is None:
        raise HTTPException(status_code=404, detail=f"Rule {rule_id} not found")

    changes = payload.model_dump(exclude_unset=True)
    if "campaign_id" in changes or "group_id" in changes:
        _check_references(
            db,
            changes.get("campaign_id", rule.campaign_id),
            changes.get("group_id", rule.group_id),
        )

    # Validate the *merged* rule before touching the row. Without this a partial
    # update could store a rule that RuleRead cannot serialise, which would break
    # GET /rules for every rule, not just this one.
    merged = {**RuleRead.model_validate(rule).model_dump(), **changes}
    merged.pop("id", None)
    merged.pop("created_at", None)
    merged.pop("updated_at", None)
    try:
        RuleCreate.model_validate(merged)
    except ValidationError as exc:
        raise HTTPException(status_code=422, detail=_first_error(exc)) from exc

    for key, value in changes.items():
        setattr(rule, key, value)
    db.commit()
    db.refresh(rule)
    return RuleRead.model_validate(rule)


def _first_error(exc: ValidationError) -> str:
    errors = exc.errors()
    if not errors:
        return "invalid rule"
    message = str(errors[0].get("msg", "invalid rule"))
    return message.removeprefix("Value error, ")


@router.delete("/{rule_id}", response_model=Message)
def delete_rule(rule_id: int, db: Session = Depends(get_db)) -> Message:
    rule = db.get(Rule, rule_id)
    if rule is None:
        raise HTTPException(status_code=404, detail=f"Rule {rule_id} not found")
    name = rule.name
    db.delete(rule)
    db.commit()
    return Message(detail=f"Deleted rule '{name}'")
