"""Loads rules from the database, scores leads and records the audit trail."""

from __future__ import annotations

from typing import Any

from sqlalchemy import select
from sqlalchemy.orm import Session

from app.models import Campaign, Lead, LeadScore, Rule, RuleGroup
from app.models.enums import ActivityType, RuleAction
from app.rules.defaults import derive_campaign_rules
from app.rules.engine import EvaluationResult, RuleGroupSpec, RuleSpec, evaluate_rules
from app.rules.scoring import is_qualified, status_for_score
from app.services.activity_service import log_activity


def load_rule_specs(db: Session, campaign: Campaign | None) -> list[RuleSpec]:
    """Collect global rules + campaign rules, falling back to derived campaign rules.

    Derived scoring rules are only added when the campaign has no stored scoring
    rules of its own, so explicit rules always win over the implicit ones.
    """
    # Rules that belong to a group are evaluated by that group, never standalone,
    # otherwise a grouped rule would score twice and bypass its all/any gate.
    ungrouped = Rule.group_id.is_(None)

    global_rules = db.scalars(
        select(Rule).where(Rule.campaign_id.is_(None), Rule.is_active.is_(True), ungrouped)
    ).all()
    specs = [RuleSpec.from_orm(rule, source="global") for rule in global_rules]

    if campaign is None:
        return specs

    campaign_rules = db.scalars(
        select(Rule).where(Rule.campaign_id == campaign.id, Rule.is_active.is_(True), ungrouped)
    ).all()
    specs.extend(RuleSpec.from_orm(rule, source="campaign-custom") for rule in campaign_rules)

    # "Does this campaign score itself?" must count grouped rules too - they are
    # evaluated via their group, but they are still the user's own scoring rules,
    # so their presence must suppress the derived fallback set.
    has_custom_scoring = (
        db.scalars(
            select(Rule.id).where(
                Rule.campaign_id == campaign.id,
                Rule.is_active.is_(True),
                Rule.action == RuleAction.SCORE,
            )
        ).first()
        is not None
    )
    specs.extend(derive_campaign_rules(campaign, include_scoring=not has_custom_scoring))
    return specs


def load_group_specs(db: Session, campaign: Campaign | None) -> list[RuleGroupSpec]:
    query = select(RuleGroup).where(RuleGroup.is_active.is_(True))
    if campaign is not None:
        query = query.where(
            (RuleGroup.campaign_id == campaign.id) | (RuleGroup.campaign_id.is_(None))
        )
    else:
        query = query.where(RuleGroup.campaign_id.is_(None))

    groups: list[RuleGroupSpec] = []
    for group in db.scalars(query).all():
        rules = [RuleSpec.from_orm(rule, source="group") for rule in group.rules if rule.is_active]
        if rules:
            groups.append(
                RuleGroupSpec(
                    id=group.id,
                    name=group.name,
                    match_type=group.match_type,
                    rules=rules,
                    is_active=group.is_active,
                    priority=group.priority,
                )
            )
    return groups


def evaluate_lead(db: Session, lead: Lead, campaign: Campaign | None = None) -> EvaluationResult:
    """Run the rule engine for a lead without persisting anything."""
    campaign = campaign or lead.campaign
    specs = load_rule_specs(db, campaign)
    groups = load_group_specs(db, campaign)
    return evaluate_rules(specs, lead, groups)


def score_lead(
    db: Session,
    lead: Lead,
    campaign: Campaign | None = None,
    persist: bool = True,
    record_history: bool = True,
) -> EvaluationResult:
    """Score a lead and write the result (plus a LeadScore snapshot) back."""
    campaign = campaign or lead.campaign
    result = evaluate_lead(db, lead, campaign)

    if not persist:
        return result

    lead.score = result.score
    lead.score_reasons = result.reasons
    lead.is_rejected = result.rejected
    lead.rejection_reason = result.rejection_reason
    lead.status = status_for_score(result.score, result.rejected).value
    if result.tags:
        merged = list(lead.tags or [])
        for tag in result.tags:
            if tag not in merged:
                merged.append(tag)
        lead.tags = merged

    if record_history:
        snapshot = LeadScore(
            lead_id=lead.id,
            campaign_id=campaign.id if campaign else None,
            score=result.score,
            raw_score=result.raw_score,
            status=lead.status,
            rejected=result.rejected,
            reasons=result.reasons,
            rule_results=result.rule_results_as_dicts(),
        )
        db.add(snapshot)
        log_activity(
            db,
            ActivityType.LEAD_SCORED,
            lead=lead,
            campaign=campaign,
            description=f"Scored {result.score}/100 ({lead.status})",
            payload={"score": result.score, "rejected": result.rejected},
        )

    db.flush()
    return result


def rescore_campaign(db: Session, campaign: Campaign) -> dict[str, Any]:
    """Re-run scoring for every lead in a campaign. Returns a summary."""
    leads = db.scalars(select(Lead).where(Lead.campaign_id == campaign.id)).all()
    summary = {"total": len(leads), "qualified": 0, "rejected": 0, "hot": 0}
    for lead in leads:
        result = score_lead(db, lead, campaign, record_history=False)
        if result.rejected:
            summary["rejected"] += 1
        elif is_qualified(result.score, result.rejected, campaign.min_score):
            summary["qualified"] += 1
        if result.score >= 85:
            summary["hot"] += 1
    db.commit()
    return summary
