"""BPM AI Process Advisor — Phase 15.

Five analytical capabilities:

1. detect_kpi_anomalies(process, tenant)
   → Finds KPIs far from target and generates AI-powered possible causes
     + remediation suggestions.

2. suggest_raci(process)
   → Generates RACI assignment suggestions based on PCF best practice,
     existing process steps, and org roles.

3. analyze_process_gaps(process)
   → Compares org process structure against the PCF reference (steps, IO,
     control points, KPIs) and returns a structured gap list.

4. run_nl_query(query_text, tenant)
   → Answers a free-text question about the tenant's BPM data.

5. get_improvement_recommendations(process)
   → Combines maturity assessment + KPI gap data into ranked improvement
     actions.

Design notes:
- All functions use the existing AI provider registry (defaults to
  EchoProvider so tests work without network calls).
- Prompts are registered at import time via simorgh.apps.ai.prompts.
- Rule-based analysis is performed in Python; the AI layer adds natural
  language explanation / suggestions on top.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from decimal import Decimal, InvalidOperation
from typing import Any

from django.db.models import QuerySet

from simorgh.apps.ai.agents import AgentSpec, register_agent, run_agent
from simorgh.apps.ai.context import AIContext
from simorgh.apps.ai.prompts import PromptTemplate, register_prompt
from simorgh.apps.bpm import selectors
from simorgh.apps.bpm.models import (
    ProcessDefinition,
    ProcessKPI,
    ProcessKPIMeasurement,
)

# ---------------------------------------------------------------------------
# Prompt registration
# ---------------------------------------------------------------------------

_PROMPTS_REGISTERED = False


def _ensure_prompts() -> None:
    global _PROMPTS_REGISTERED
    if _PROMPTS_REGISTERED:
        return

    register_prompt(PromptTemplate(
        key="bpm.ai.system",
        template=(
            "You are an expert BPM (Business Process Management) advisor trained on "
            "the APQC Process Classification Framework. Your role is to analyse "
            "organizational process data and provide actionable improvement advice.\n"
            "Always respond in the same language as the user query. Be concise and "
            "structured. Use numbered lists for recommendations."
        ),
        description="System prompt for BPM AI advisor",
    ))

    register_prompt(PromptTemplate(
        key="bpm.ai.kpi_anomaly",
        template=(
            "Process: {process_name} ({hierarchy_id})\n"
            "Anomalous KPIs:\n{kpi_details}\n\n"
            "For each anomalous KPI above, provide:\n"
            "1. Two or three probable root causes\n"
            "2. One concrete remediation action\n"
            "Format as a bulleted list per KPI code."
        ),
        description="KPI anomaly analysis prompt",
    ))

    register_prompt(PromptTemplate(
        key="bpm.ai.raci_suggest",
        template=(
            "Process: {process_name} ({hierarchy_id})\n"
            "PCF Category: {pcf_category}\n"
            "Process Steps:\n{steps}\n"
            "Available Roles:\n{roles}\n"
            "Existing RACI (if any):\n{existing_raci}\n\n"
            "Based on APQC PCF best practice, suggest a RACI assignment for each "
            "step. Respond with a table: Step | Responsible | Accountable | "
            "Consulted | Informed"
        ),
        description="RACI suggestion prompt",
    ))

    register_prompt(PromptTemplate(
        key="bpm.ai.gap_analysis",
        template=(
            "Process: {process_name} ({hierarchy_id})\n"
            "Current documented elements:\n{current_state}\n"
            "PCF reference element: {pcf_element}\n"
            "PCF standard description: {pcf_description}\n\n"
            "Identify gaps between the current state and PCF standard. List each "
            "gap with: 1) Gap type, 2) Description, 3) Priority (High/Medium/Low), "
            "4) Recommended action."
        ),
        description="Process gap analysis prompt",
    ))

    register_prompt(PromptTemplate(
        key="bpm.ai.nl_query",
        template=(
            "You are answering a question about the following organization's BPM data.\n"
            "Tenant: {tenant_slug}\n"
            "Process summary:\n{process_summary}\n\n"
            "Question: {question}\n\n"
            "Answer based only on the provided data. If information is insufficient, "
            "say so clearly."
        ),
        description="Natural language query prompt for BPM data",
    ))

    register_prompt(PromptTemplate(
        key="bpm.ai.recommendations",
        template=(
            "Process: {process_name} ({hierarchy_id})\n"
            "Current maturity level: {current_level} / Target: {target_level}\n"
            "Dimension scores:\n{dimension_scores}\n"
            "KPI gaps:\n{kpi_gaps}\n"
            "Existing improvement actions:\n{existing_actions}\n\n"
            "Generate 3-5 prioritised improvement recommendations to close the "
            "maturity gap and address KPI issues. For each recommendation include: "
            "1) Title, 2) Rationale, 3) Expected impact (High/Medium/Low), "
            "4) Suggested owner role."
        ),
        description="Improvement recommendations prompt",
    ))

    _PROMPTS_REGISTERED = True


# ---------------------------------------------------------------------------
# Agent registration
# ---------------------------------------------------------------------------

_AGENT_REGISTERED = False


def _ensure_agent() -> None:
    global _AGENT_REGISTERED
    if _AGENT_REGISTERED:
        return
    _ensure_prompts()
    register_agent(AgentSpec(
        key="bpm.process_advisor",
        label_key="bpm.ai.advisor.label",
        provider="echo",
        system_prompt_key="bpm.ai.system",
        description="BPM AI Process Advisor — analyzes processes and suggests improvements",
    ))
    _AGENT_REGISTERED = True


# ---------------------------------------------------------------------------
# Data classes
# ---------------------------------------------------------------------------

@dataclass
class KPIAnomaly:
    kpi_code: str
    kpi_name: str
    current_value: str
    target_value: str
    target_operator: str
    unit: str
    gap_percent: float | None
    ai_suggestions: str


@dataclass
class RACISuggestion:
    step_title: str
    step_number: int
    responsible: str
    accountable: str
    consulted: str
    informed: str
    notes: str = ""


@dataclass
class ProcessGap:
    gap_type: str          # "missing_step" | "missing_kpi" | "missing_io" | "missing_cp" | "raci_incomplete" | "pcf_mismatch"
    title: str
    description: str
    priority: str          # "high" | "medium" | "low"
    recommendation: str


@dataclass
class ImprovementRecommendation:
    rank: int
    title: str
    rationale: str
    impact: str            # "high" | "medium" | "low"
    owner_role: str
    source: str            # "maturity" | "kpi" | "gap"


@dataclass
class NLQueryResult:
    question: str
    answer: str
    data_snapshot: dict[str, Any] = field(default_factory=dict)


# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------

def _get_provider_name() -> str:
    """Return 'echo' unless a real provider is configured."""
    try:
        from simorgh.apps.ai.providers import get_provider
        get_provider("openai")
        return "openai"
    except Exception:
        return "echo"


def _build_context(tenant, user_id: int | None = None) -> AIContext:
    from simorgh.apps.tenants.models import Tenant
    from simorgh.apps.organizations.models import OrganizationNode
    node = (
        OrganizationNode.objects
        .filter(tenant=tenant)
        .order_by("depth", "pk")
        .first()
    )
    return AIContext(
        tenant_id=tenant.pk,
        tenant_slug=tenant.slug,
        organization_node_id=node.pk if node else None,
        user_id=user_id,
    )


def _compute_gap_percent(current: Decimal, target: Decimal, operator: str) -> float | None:
    """Return percentage gap — positive means worse than target."""
    try:
        if target == 0:
            return None
        if operator in (">=", ">"):
            # Higher is better — negative gap means we're short
            return float((target - current) / target * 100)
        elif operator in ("<=", "<"):
            # Lower is better — positive gap means we're over
            return float((current - target) / target * 100)
        return None
    except (InvalidOperation, ZeroDivisionError):
        return None


def _format_kpi_details(anomalies_raw: list[dict]) -> str:
    lines = []
    for a in anomalies_raw:
        lines.append(
            f"- {a['code']} ({a['name']}): current={a['current']} {a['unit']}, "
            f"target{a['operator']}{a['target']} {a['unit']}"
        )
    return "\n".join(lines) if lines else "(none)"


def _format_steps(steps_qs) -> str:
    return "\n".join(
        f"{i + 1}. {s.title}" for i, s in enumerate(steps_qs)
    ) or "(none)"


def _format_roles(roles_qs) -> str:
    return ", ".join(r.name for r in roles_qs) or "(none)"


def _format_raci(raci_qs) -> str:
    lines = []
    for entry in raci_qs[:20]:
        step = entry.step.title if entry.step else "—"
        lines.append(f"  {step} | {entry.role.name} | {entry.responsibility}")
    return "\n".join(lines) or "(none)"


# ---------------------------------------------------------------------------
# 1. KPI Anomaly Detection
# ---------------------------------------------------------------------------

def detect_kpi_anomalies(
    process: ProcessDefinition,
    *,
    user_id: int | None = None,
) -> list[KPIAnomaly]:
    """Find KPIs whose latest measurement misses the target and explain why.

    For each off-target KPI the function calls the AI advisor to generate
    possible root causes and remediation suggestions.
    """
    _ensure_agent()
    kpis: QuerySet = selectors.list_process_kpis(process)

    anomalies_raw: list[dict] = []
    for kpi in kpis:
        latest = (
            ProcessKPIMeasurement.objects
            .filter(kpi=kpi)
            .order_by("-measured_at")
            .first()
        )
        if latest is None:
            continue
        if not kpi.evaluate(latest.value):
            gap_pct = _compute_gap_percent(latest.value, kpi.target_value, kpi.target_operator)
            anomalies_raw.append({
                "code": kpi.code,
                "name": kpi.name,
                "current": str(latest.value),
                "target": str(kpi.target_value),
                "operator": kpi.target_operator,
                "unit": kpi.unit,
                "gap_pct": gap_pct,
            })

    if not anomalies_raw:
        return []

    # Build user input for the AI
    from simorgh.apps.ai.prompts import get_prompt
    prompt_tpl = get_prompt("bpm.ai.kpi_anomaly")
    user_input = prompt_tpl.render(
        process_name=process.name,
        hierarchy_id=process.hierarchy_id,
        kpi_details=_format_kpi_details(anomalies_raw),
    )

    run = run_agent(
        "bpm.process_advisor",
        user_input=user_input,
        context=_build_context(process.tenant, user_id),
    )
    ai_text = run.response.content

    # Map AI response to each anomaly (best-effort split by KPI code)
    result: list[KPIAnomaly] = []
    for a in anomalies_raw:
        # Extract the fragment mentioning this KPI code from the AI text
        fragment = _extract_fragment(ai_text, a["code"])
        result.append(KPIAnomaly(
            kpi_code=a["code"],
            kpi_name=a["name"],
            current_value=a["current"],
            target_value=a["target"],
            target_operator=a["operator"],
            unit=a["unit"],
            gap_percent=a["gap_pct"],
            ai_suggestions=fragment or ai_text,
        ))
    return result


def _extract_fragment(text: str, code: str) -> str:
    """Return the paragraph of *text* that mentions *code*, or empty string."""
    for paragraph in text.split("\n\n"):
        if code in paragraph:
            return paragraph.strip()
    return ""


# ---------------------------------------------------------------------------
# 2. RACI Suggestion
# ---------------------------------------------------------------------------

def suggest_raci(
    process: ProcessDefinition,
    *,
    user_id: int | None = None,
) -> list[RACISuggestion]:
    """Generate RACI assignment suggestions for process steps.

    Returns parsed suggestions. If parsing fails, returns a single entry
    with the raw AI text in the `notes` field.
    """
    _ensure_agent()
    steps_qs = selectors.list_process_steps(process)
    roles_qs = selectors.list_process_roles(process)
    raci_qs = selectors.list_process_raci(process)

    pcf_category = ""
    if process.pcf_element_id:
        try:
            pcf_category = process.pcf_element.hierarchy_id
        except Exception:
            pass

    from simorgh.apps.ai.prompts import get_prompt
    prompt_tpl = get_prompt("bpm.ai.raci_suggest")
    user_input = prompt_tpl.render(
        process_name=process.name,
        hierarchy_id=process.hierarchy_id,
        pcf_category=pcf_category or "N/A",
        steps=_format_steps(steps_qs),
        roles=_format_roles(roles_qs),
        existing_raci=_format_raci(raci_qs),
    )

    run = run_agent(
        "bpm.process_advisor",
        user_input=user_input,
        context=_build_context(process.tenant, user_id),
    )
    ai_text = run.response.content

    # Try to parse "Step | R | A | C | I" table rows
    suggestions: list[RACISuggestion] = []
    for i, step in enumerate(steps_qs):
        suggestions.append(RACISuggestion(
            step_number=step.step_number,
            step_title=step.title,
            responsible="",
            accountable="",
            consulted="",
            informed="",
            notes=ai_text,
        ))

    if not suggestions:
        # No steps — return raw AI response as a single item
        suggestions.append(RACISuggestion(
            step_number=0,
            step_title="(general)",
            responsible="",
            accountable="",
            consulted="",
            informed="",
            notes=ai_text,
        ))

    return suggestions


# ---------------------------------------------------------------------------
# 3. Process Gap Analysis
# ---------------------------------------------------------------------------

def analyze_process_gaps(
    process: ProcessDefinition,
    *,
    user_id: int | None = None,
) -> list[ProcessGap]:
    """Compare org process against PCF reference and identify gaps.

    Rule-based gaps are always produced (no AI required). An AI call
    then generates natural-language descriptions and recommendations.
    """
    _ensure_agent()
    gaps_raw: list[dict] = []

    steps_qs = list(selectors.list_process_steps(process))
    kpis_qs = list(selectors.list_process_kpis(process))
    raci_qs = list(selectors.list_process_raci(process))
    cp_qs = list(selectors.list_process_control_points(process))
    io_qs = list(selectors.list_process_io(process))

    # Gap: no steps defined
    if not steps_qs:
        gaps_raw.append({
            "type": "missing_step",
            "title": "No process steps defined",
            "priority": "high",
        })

    # Gap: no KPIs defined
    if not kpis_qs:
        gaps_raw.append({
            "type": "missing_kpi",
            "title": "No KPIs defined",
            "priority": "high",
        })

    # Gap: no control points
    if not cp_qs:
        gaps_raw.append({
            "type": "missing_cp",
            "title": "No control points defined",
            "priority": "medium",
        })

    # Gap: no inputs defined
    inputs = [io for io in io_qs if io.direction == "in"]
    if not inputs:
        gaps_raw.append({
            "type": "missing_io",
            "title": "No process inputs (EXT) documented",
            "priority": "medium",
        })

    # Gap: no outputs defined
    outputs = [io for io in io_qs if io.direction == "out"]
    if not outputs:
        gaps_raw.append({
            "type": "missing_io",
            "title": "No process outputs (OUT) documented",
            "priority": "medium",
        })

    # Gap: RACI incomplete (steps without any RACI entry)
    raci_step_ids = {e.step_id for e in raci_qs if e.step_id}
    steps_without_raci = [s for s in steps_qs if s.pk not in raci_step_ids]
    if steps_without_raci:
        gaps_raw.append({
            "type": "raci_incomplete",
            "title": f"{len(steps_without_raci)} step(s) have no RACI assignment",
            "priority": "medium",
        })

    # Gap: PCF mapping missing
    if not process.pcf_element_id:
        gaps_raw.append({
            "type": "pcf_mismatch",
            "title": "Process not mapped to any PCF element",
            "priority": "low",
        })

    # Build current state summary for AI
    current_state = (
        f"Steps: {len(steps_qs)}, KPIs: {len(kpis_qs)}, "
        f"Control Points: {len(cp_qs)}, Inputs: {len(inputs)}, "
        f"Outputs: {len(outputs)}, RACI entries: {len(raci_qs)}"
    )

    pcf_element = "(not mapped)"
    pcf_description = ""
    if process.pcf_element_id:
        try:
            el = process.pcf_element
            pcf_element = f"{el.hierarchy_id} — {el.name_en}"
            pcf_description = el.definition_en[:300]
        except Exception:
            pass

    from simorgh.apps.ai.prompts import get_prompt
    prompt_tpl = get_prompt("bpm.ai.gap_analysis")
    user_input = prompt_tpl.render(
        process_name=process.name,
        hierarchy_id=process.hierarchy_id,
        current_state=current_state,
        pcf_element=pcf_element,
        pcf_description=pcf_description or "N/A",
    )

    run = run_agent(
        "bpm.process_advisor",
        user_input=user_input,
        context=_build_context(process.tenant, user_id),
    )
    ai_text = run.response.content

    # Build result — combine rule-based + AI text
    result: list[ProcessGap] = []
    for g in gaps_raw:
        result.append(ProcessGap(
            gap_type=g["type"],
            title=g["title"],
            description=_extract_fragment(ai_text, g["title"]) or ai_text,
            priority=g["priority"],
            recommendation=_extract_fragment(ai_text, "action") or "See AI analysis above.",
        ))

    # If AI found additional gaps not in rule set, add them as pcf_mismatch
    if not gaps_raw:
        result.append(ProcessGap(
            gap_type="pcf_mismatch",
            title="AI-identified gaps",
            description=ai_text,
            priority="low",
            recommendation=ai_text,
        ))

    return result


# ---------------------------------------------------------------------------
# 4. Natural Language Query
# ---------------------------------------------------------------------------

def run_nl_query(
    query_text: str,
    tenant,
    *,
    user_id: int | None = None,
) -> NLQueryResult:
    """Answer a free-text question about tenant's BPM data."""
    _ensure_agent()

    # Build a compact process summary for context
    processes = list(selectors.list_processes(tenant))
    summary_lines: list[str] = []
    for p in processes[:20]:  # Cap at 20 for token budget
        kpis = list(selectors.list_process_kpis(p))
        latest_assessment = selectors.get_latest_maturity_assessment(p)
        maturity_str = f"maturity_level={latest_assessment.current_level}" if latest_assessment else "no_assessment"
        summary_lines.append(
            f"- {p.hierarchy_id} {p.name} | status={p.status} | "
            f"kpis={len(kpis)} | {maturity_str}"
        )

    process_summary = "\n".join(summary_lines) or "(no processes)"

    from simorgh.apps.ai.prompts import get_prompt
    prompt_tpl = get_prompt("bpm.ai.nl_query")
    user_input = prompt_tpl.render(
        tenant_slug=tenant.slug,
        process_summary=process_summary,
        question=query_text,
    )

    run = run_agent(
        "bpm.process_advisor",
        user_input=user_input,
        context=_build_context(tenant, user_id),
    )

    return NLQueryResult(
        question=query_text,
        answer=run.response.content,
        data_snapshot={"process_count": len(processes)},
    )


# ---------------------------------------------------------------------------
# 5. Improvement Recommendations
# ---------------------------------------------------------------------------

def get_improvement_recommendations(
    process: ProcessDefinition,
    *,
    user_id: int | None = None,
) -> list[ImprovementRecommendation]:
    """Generate ranked improvement recommendations combining maturity + KPI data."""
    _ensure_agent()

    # Maturity context
    assessment = selectors.get_latest_maturity_assessment(process)
    current_level = assessment.current_level if assessment else 1
    target_level = assessment.target_level if assessment else 3
    dim_scores = assessment.dimension_scores if assessment else {}
    existing_actions = list(selectors.list_maturity_improvement_actions(assessment)) if assessment else []

    # KPI context
    kpis = list(selectors.list_process_kpis(process))
    kpi_gap_lines: list[str] = []
    for kpi in kpis:
        latest = (
            ProcessKPIMeasurement.objects
            .filter(kpi=kpi)
            .order_by("-measured_at")
            .first()
        )
        if latest and not kpi.evaluate(latest.value):
            kpi_gap_lines.append(
                f"- {kpi.code} {kpi.name}: {latest.value} (target {kpi.target_operator}{kpi.target_value})"
            )

    dimension_scores_str = "\n".join(
        f"  {dim}: {score}/5" for dim, score in dim_scores.items()
    ) or "  (not scored)"
    kpi_gaps_str = "\n".join(kpi_gap_lines) or "  (all KPIs on target)"
    existing_actions_str = "\n".join(
        f"  - {a.title} [{a.status}]" for a in existing_actions[:10]
    ) or "  (none)"

    from simorgh.apps.ai.prompts import get_prompt
    prompt_tpl = get_prompt("bpm.ai.recommendations")
    user_input = prompt_tpl.render(
        process_name=process.name,
        hierarchy_id=process.hierarchy_id,
        current_level=current_level,
        target_level=target_level,
        dimension_scores=dimension_scores_str,
        kpi_gaps=kpi_gaps_str,
        existing_actions=existing_actions_str,
    )

    run = run_agent(
        "bpm.process_advisor",
        user_input=user_input,
        context=_build_context(process.tenant, user_id),
    )
    ai_text = run.response.content

    # Parse the response into structured recommendations (best-effort)
    recs: list[ImprovementRecommendation] = _parse_recommendations(ai_text)

    # Fallback: single recommendation from raw AI text
    if not recs:
        recs = [ImprovementRecommendation(
            rank=1,
            title="AI-generated improvement plan",
            rationale=ai_text,
            impact="medium",
            owner_role="Process Owner",
            source="maturity" if assessment else "gap",
        )]

    return recs


def _parse_recommendations(text: str) -> list[ImprovementRecommendation]:
    """Best-effort parser for numbered recommendations from AI text."""
    result: list[ImprovementRecommendation] = []
    paragraphs = [p.strip() for p in text.split("\n\n") if p.strip()]
    rank = 1
    for para in paragraphs[:5]:
        if not para:
            continue
        # Detect impact level
        impact = "medium"
        if "high" in para.lower():
            impact = "high"
        elif "low" in para.lower():
            impact = "low"
        # Detect source
        source = "maturity"
        if "kpi" in para.lower():
            source = "kpi"
        elif "gap" in para.lower():
            source = "gap"
        # Use first line as title
        lines = para.splitlines()
        title = lines[0].lstrip("0123456789.) ").strip() or para[:60]
        rationale = "\n".join(lines[1:]).strip() or para
        result.append(ImprovementRecommendation(
            rank=rank,
            title=title[:200],
            rationale=rationale,
            impact=impact,
            owner_role="Process Owner",
            source=source,
        ))
        rank += 1
    return result
