"""AI-driven engagement analysis service."""

from __future__ import annotations

import json
import logging
from datetime import datetime, timezone
from typing import Any

from sqlalchemy import select
from sqlalchemy.orm import Session

from app.models.engagement import Engagement
from app.models.event import Event, EventPhase
from app.schemas.ai_analysis import AIAnalysisRead
from app.services import coverage_engine
from app.services.ai import provider_registry as ai_provider_registry
from app.services.ai import (
    AIProviderConfigError,
    AIProviderResponseError,
    AIProviderUnavailableError,
)

MAX_EVENTS_IN_PROMPT = 200
MAX_EVENT_CONTENT_CHARS = 500
MAX_EVENTS_CONTEXT_CHARS = 15_000
REQUIRED_RESPONSE_KEYS = {
    "executive_summary",
    "technical_summary",
    "next_actions",
    "attack_hypotheses",
    "risk_notes",
}
LOGGER = logging.getLogger("sentineledge.ai.analysis")


class AIConfigError(RuntimeError):
    """Raised when selected AI provider is unavailable or misconfigured."""


class AIAnalysisError(RuntimeError):
    """Raised when AI provider output cannot be used for analysis."""


def _format_ts(ts: datetime) -> str:
    """Format datetime for prompt readability."""
    with_timezone = ts if ts.tzinfo else ts.replace(tzinfo=timezone.utc)
    return with_timezone.astimezone(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")


def _truncate_text(value: str, max_chars: int = MAX_EVENT_CONTENT_CHARS) -> str:
    """Truncate long text sections while keeping context."""
    clean = " ".join(value.split())
    if len(clean) <= max_chars:
        return clean
    return f"{clean[: max_chars - 3]}..."


def _normalize_str_list(value: Any) -> list[str]:
    """Normalize model output lists to clean string arrays."""
    if not isinstance(value, list):
        return []

    result: list[str] = []
    for item in value:
        if isinstance(item, str):
            text = item.strip()
            if text:
                result.append(text)
    return result


def _event_row(event: Event) -> dict[str, Any]:
    """Build one compact event row for the model context."""
    return {
        "id": event.id,
        "ts": _format_ts(event.ts),
        "phase": event.phase.value,
        "severity": event.severity.value,
        "source": event.source,
        "title": event.title,
        "tags": event.tags or [],
        "content": _truncate_text(event.content),
    }


def _build_events_context(events: list[Event]) -> tuple[list[dict[str, Any]], int]:
    """Build compact event context with max length guard."""
    scoped_events = events[-MAX_EVENTS_IN_PROMPT:]
    selected_rows: list[dict[str, Any]] = []
    used_chars = 0

    # Keep latest events first when enforcing the prompt-size cap.
    for event in reversed(scoped_events):
        row = _event_row(event)
        row_chars = len(json.dumps(row, ensure_ascii=True))
        if selected_rows and used_chars + row_chars > MAX_EVENTS_CONTEXT_CHARS:
            continue
        if not selected_rows and row_chars > MAX_EVENTS_CONTEXT_CHARS:
            row["content"] = _truncate_text(event.content, max_chars=180)
            row_chars = len(json.dumps(row, ensure_ascii=True))
            if row_chars > MAX_EVENTS_CONTEXT_CHARS:
                row["content"] = ""
                row_chars = len(json.dumps(row, ensure_ascii=True))

        if used_chars + row_chars > MAX_EVENTS_CONTEXT_CHARS:
            continue

        selected_rows.append(row)
        used_chars += row_chars

    selected_rows.reverse()
    truncated_count = max(0, len(events) - len(selected_rows))
    return selected_rows, truncated_count


def _build_prompt(
    engagement: Engagement,
    events: list[Event],
    coverage: Any,
) -> str:
    """Build strict JSON-analysis prompt for provider models."""
    event_rows, truncated_count = _build_events_context(events)
    context = {
        "engagement": {
            "id": engagement.id,
            "name": engagement.name,
            "created_at": _format_ts(engagement.created_at),
        },
        "phases_reference": [phase.value for phase in EventPhase],
        "coverage": {
            "coverage_score": coverage.coverage_score,
            "phases": coverage.phases,
            "gaps": coverage.gaps,
            "recommendations": coverage.recommendations,
        },
        "event_summary": {
            "total_events": len(events),
            "events_in_prompt": len(event_rows),
            "truncated_count": truncated_count,
            "max_events_chars": MAX_EVENTS_CONTEXT_CHARS,
        },
        "events": event_rows,
    }

    return (
        "You are a senior offensive security analyst.\n"
        "Return ONLY valid JSON (single object), with no markdown, no code fences, and no extra keys.\n"
        "Use this exact schema:\n"
        "{\n"
        '  "executive_summary": "string",\n'
        '  "technical_summary": "string",\n'
        '  "next_actions": ["string"],\n'
        '  "attack_hypotheses": ["string"],\n'
        '  "risk_notes": ["string"]\n'
        "}\n"
        "Generate concise but actionable outputs based on phases, severities, tags, and coverage gaps.\n"
        f"Context:\n{json.dumps(context, ensure_ascii=True, indent=2)}"
    )


def _validate_analysis_payload(engagement_id: int, payload: Any) -> AIAnalysisRead:
    """Validate model payload and map into stable API schema."""
    if not isinstance(payload, dict):
        raise AIAnalysisError("AI provider returned invalid payload (expected JSON object)")

    missing = [key for key in REQUIRED_RESPONSE_KEYS if key not in payload]
    if missing:
        missing_text = ", ".join(sorted(missing))
        raise AIAnalysisError(f"AI provider response missing required keys: {missing_text}")

    return AIAnalysisRead(
        engagement_id=engagement_id,
        executive_summary=str(payload["executive_summary"]).strip(),
        technical_summary=str(payload["technical_summary"]).strip(),
        next_actions=_normalize_str_list(payload["next_actions"]),
        attack_hypotheses=_normalize_str_list(payload["attack_hypotheses"]),
        risk_notes=_normalize_str_list(payload["risk_notes"]),
    )


def _parse_provider_payload(raw_text: str) -> dict[str, Any]:
    """Parse provider text as JSON object with light cleanup."""
    text = str(raw_text or "").strip()
    if text.startswith("```"):
        lines = [line for line in text.splitlines() if not line.strip().startswith("```")]
        text = "\n".join(lines).strip()

    try:
        payload = json.loads(text)
    except json.JSONDecodeError:
        start = text.find("{")
        end = text.rfind("}")
        if start < 0 or end <= start:
            raise AIAnalysisError("AI provider returned non-JSON analysis output")
        candidate = text[start : end + 1]
        try:
            payload = json.loads(candidate)
        except json.JSONDecodeError as exc:
            raise AIAnalysisError("AI provider returned invalid JSON analysis output") from exc

    if not isinstance(payload, dict):
        raise AIAnalysisError("AI provider returned JSON payload that is not an object")
    return payload


def analyze_engagement(db: Session, engagement_id: int) -> dict[str, Any]:
    """Analyze engagement events and return structured AI insights."""
    engagement = db.scalar(select(Engagement).where(Engagement.id == engagement_id))
    if engagement is None:
        raise LookupError("engagement not found")

    events = list(
        db.scalars(
            select(Event)
            .where(Event.engagement_id == engagement_id)
            .order_by(Event.ts.asc(), Event.id.asc()),
        ).all(),
    )
    coverage = coverage_engine.get_coverage(db=db, engagement_id=engagement_id)
    prompt = _build_prompt(engagement=engagement, events=events, coverage=coverage)
    LOGGER.info(
        "ai_analysis_service.analyze_engagement engagement_id=%s events=%s prompt_len=%s",
        engagement_id,
        len(events),
        len(prompt),
    )

    try:
        selection = ai_provider_registry.resolve_generation_provider()
    except (AIProviderConfigError, AIProviderUnavailableError) as exc:
        LOGGER.warning(
            "ai_analysis_service provider_unavailable error_class=%s error=%s",
            exc.__class__.__name__,
            str(exc),
        )
        raise AIConfigError(str(exc)) from exc

    try:
        LOGGER.info(
            "ai_analysis_service provider_call selected=%s effective=%s",
            selection.selected_provider,
            selection.effective_provider,
        )
        raw_response = selection.provider.generate(prompt=prompt)
    except (AIProviderConfigError, AIProviderUnavailableError) as exc:
        LOGGER.warning(
            "ai_analysis_service provider_config_error error_class=%s error=%s",
            exc.__class__.__name__,
            str(exc),
        )
        raise AIConfigError(str(exc)) from exc
    except AIProviderResponseError as exc:
        LOGGER.warning(
            "ai_analysis_service provider_response_error error_class=%s error=%s",
            exc.__class__.__name__,
            str(exc),
        )
        raise AIAnalysisError(str(exc)) from exc

    payload = _parse_provider_payload(raw_response)
    analysis = _validate_analysis_payload(engagement_id=engagement_id, payload=payload)
    return analysis.model_dump()
