"""AI-enhanced professional markdown report generator."""

from __future__ import annotations

from datetime import datetime, timezone
from pathlib import Path
import re
from typing import Any

from sqlalchemy import select
from sqlalchemy.orm import Session

from app.core.config import settings
from app.models.event import Event, EventPhase, SeverityLevel
from app.schemas.coverage import CoverageSummaryRead
from app.schemas.timeline import TimelineEventRead
from app.services import ai_analysis_service, coverage_engine, timeline, vault
from app.services.engagements import get_engagement_by_id

MAX_FINDING_CONTENT_LEN = 500
MAX_APPENDIX_CONTENT_LEN = 900
MAX_APPENDIX_TOTAL_CHARS = 40_000
MAX_VAULT_EVIDENCE_ITEMS = 10
MAX_VAULT_EVIDENCE_EXCERPT = 280

PHASE_LABELS: dict[str, str] = {
    EventPhase.RECON.value: "Reconnaissance",
    EventPhase.ENUMERATION.value: "Enumeration",
    EventPhase.INITIAL_ACCESS.value: "Initial Access",
    EventPhase.PRIV_ESC.value: "Privilege Escalation",
    EventPhase.POST_EXPLOITATION.value: "Post-Exploitation",
    EventPhase.CLEANUP.value: "Cleanup",
}

PHASE_METHOD_GUIDE: dict[str, str] = {
    EventPhase.RECON.value: "Asset discovery, attack-surface mapping, and passive intelligence collection.",
    EventPhase.ENUMERATION.value: "Service enumeration, technology fingerprinting, and exposure validation.",
    EventPhase.INITIAL_ACCESS.value: "Initial foothold attempts against validated attack vectors.",
    EventPhase.PRIV_ESC.value: "Privilege boundary testing and escalation-path validation.",
    EventPhase.POST_EXPLOITATION.value: "Post-compromise validation, persistence checks, and lateral movement analysis.",
    EventPhase.CLEANUP.value: "Operational cleanup, artifact reduction, and traceability controls.",
}

PHASE_CHECKLIST_MAP: dict[str, str] = {
    EventPhase.RECON.value: "Expand reconnaissance and asset inventory validation",
    EventPhase.ENUMERATION.value: "Enumerate additional services",
    EventPhase.INITIAL_ACCESS.value: "Attempt additional initial access vectors",
    EventPhase.PRIV_ESC.value: "Validate privilege escalation vectors",
    EventPhase.POST_EXPLOITATION.value: "Check persistence mechanisms",
    EventPhase.CLEANUP.value: "Perform artifact cleanup validation",
}


def _project_root() -> Path:
    """Return repository root path."""
    return Path(__file__).resolve().parents[2]


def _reports_dir() -> Path:
    """Return reports folder path."""
    return _project_root() / "reports"


def _format_dt(value: datetime) -> str:
    """Format timestamps consistently for report output."""
    dt = value if value.tzinfo else value.replace(tzinfo=timezone.utc)
    return dt.astimezone(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")


def _collapse_spaces(value: str) -> str:
    """Normalize whitespace for cleaner markdown."""
    return " ".join(value.split())


def _summarize_content(content: str, max_len: int = MAX_FINDING_CONTENT_LEN) -> str:
    """Trim long content while preserving readability."""
    text = _collapse_spaces(content)
    if len(text) <= max_len:
        return text
    return f"{text[: max_len - 3]}..."


def _events_for_report(db: Session, engagement_id: int) -> list[Event]:
    """Return all events for the engagement sorted by ts asc."""
    result = db.scalars(
        select(Event)
        .where(Event.engagement_id == engagement_id)
        .order_by(Event.ts.asc(), Event.id.asc()),
    )
    return list(result.all())


def _timeline_for_report(db: Session, engagement_id: int) -> list[TimelineEventRead]:
    """Return timeline representation sorted by timestamp asc."""
    result = timeline.get_timeline(db=db, engagement_id=engagement_id, group_by_phase=False)
    if isinstance(result, list):
        return result
    return []


def _phase_title(phase: str) -> str:
    """Map phase key to readable title."""
    return PHASE_LABELS.get(phase, phase.replace("_", " ").title())


def _deterministic_executive_summary(
    engagement_name: str,
    events: list[Event],
    coverage: CoverageSummaryRead,
) -> str:
    """Create deterministic strategic summary when AI is unavailable."""
    severity_counts = {
        level.value: sum(1 for event in events if event.severity.value == level.value)
        for level in SeverityLevel
    }
    return (
        f"{engagement_name} currently contains {len(events)} recorded events with coverage score "
        f"{coverage.coverage_score}%. Observed severity distribution is low={severity_counts['low']}, "
        f"med={severity_counts['med']}, high={severity_counts['high']}. "
        f"Priority should focus on closing uncovered phases: "
        f"{', '.join(coverage.gaps) if coverage.gaps else 'none'}."
    )


def _deterministic_risk_notes(events: list[Event], coverage: CoverageSummaryRead) -> list[str]:
    """Build deterministic risk notes if AI analysis cannot be used."""
    high_count = sum(1 for event in events if event.severity == SeverityLevel.HIGH)
    med_count = sum(1 for event in events if event.severity == SeverityLevel.MED)

    notes: list[str] = []
    if high_count > 0:
        notes.append(
            f"{high_count} high-severity event(s) indicate elevated exploitation potential and higher business impact.",
        )
    elif med_count > 0:
        notes.append(
            f"{med_count} medium-severity event(s) require escalation analysis to confirm exploitability.",
        )
    else:
        notes.append("No medium/high events recorded; current risk signal remains limited by available evidence.")

    if coverage.gaps:
        notes.append(
            f"Coverage gaps in {', '.join(coverage.gaps)} reduce confidence in final assurance.",
        )
    if coverage.coverage_score < 50:
        notes.append("Coverage score below 50% indicates significant unknowns in attack-path validation.")

    return notes


def _normalize_unique(items: list[str]) -> list[str]:
    """Deduplicate string list while preserving order."""
    unique: list[str] = []
    seen: set[str] = set()
    for item in items:
        normalized = item.strip().lower()
        if not normalized or normalized in seen:
            continue
        unique.append(item.strip())
        seen.add(normalized)
    return unique


def _build_methodology_section(events: list[Event]) -> str:
    """Render deterministic methodology from observed phases."""
    used_phases = {event.phase.value for event in events}
    phases = [phase.value for phase in EventPhase if phase.value in used_phases]
    if not phases:
        phases = [phase.value for phase in EventPhase]

    lines = []
    for phase in phases:
        lines.append(f"- **{_phase_title(phase)}**: {PHASE_METHOD_GUIDE[phase]}")
    return "\n".join(lines)


def _build_scope_section(engagement_name: str, events: list[Event]) -> str:
    """Render deterministic scope summary."""
    phase_set = {event.phase.value for event in events}
    phases = [_phase_title(phase.value) for phase in EventPhase if phase.value in phase_set]
    phase_text = ", ".join(phases) if phases else "No execution evidence captured yet"
    return "\n".join(
        [
            f"- Engagement: {engagement_name}",
            f"- Evidence records: {len(events)} event(s)",
            f"- Observed phase coverage in logs: {phase_text}",
        ],
    )


def _build_timeline_table(timeline_events: list[TimelineEventRead]) -> str:
    """Render timeline section as markdown table."""
    if not timeline_events:
        return "_No timeline events recorded for this engagement._"

    lines = [
        "| Timestamp (UTC) | Phase | Severity | Source | Title | Tags |",
        "| --- | --- | --- | --- | --- | --- |",
    ]
    for event in timeline_events:
        tags = ", ".join(event.tags) if event.tags else "-"
        lines.append(
            f"| {_format_dt(event.ts)} | {event.phase.value} | {event.severity.value} "
            f"| {event.source} | {event.title} | {tags} |",
        )
    return "\n".join(lines)


def _build_coverage_section(coverage: CoverageSummaryRead) -> str:
    """Render coverage metrics and status table."""
    in_progress = [phase for phase, status in coverage.phases.items() if status == "in_progress"]
    gap_text = ", ".join(coverage.gaps) if coverage.gaps else "none"
    in_progress_text = ", ".join(in_progress) if in_progress else "none"

    table_lines = [
        "| Phase | Status |",
        "| --- | --- |",
    ]
    table_lines.extend([f"| {phase} | {status} |" for phase, status in coverage.phases.items()])

    return "\n".join(
        [
            f"- Coverage score: {coverage.coverage_score}%",
            f"- Gaps detected: {gap_text}",
            f"- In-progress phases: {in_progress_text}",
            "",
            *table_lines,
        ],
    )


def _build_technical_findings(events: list[Event], technical_summary: str | None = None) -> str:
    """Render technical findings grouped by phase."""
    if not events:
        return "_No technical findings available._"

    grouped: dict[str, list[Event]] = {phase.value: [] for phase in EventPhase}
    for event in events:
        grouped[event.phase.value].append(event)

    lines: list[str] = []
    if technical_summary:
        lines.append(_collapse_spaces(technical_summary))
        lines.append("")

    for phase in EventPhase:
        phase_events = grouped[phase.value]
        if not phase_events:
            continue

        lines.append(f"## {_phase_title(phase.value)}")
        lines.append("")
        for event in phase_events:
            lines.append(
                f"- {_format_dt(event.ts)} | {event.severity.value} | {event.source} | "
                f"{event.title} | {_summarize_content(event.content)}",
            )
        lines.append("")

    return "\n".join(lines).strip()


def _build_checklist(events: list[Event], coverage: CoverageSummaryRead) -> tuple[list[str], list[str]]:
    """Build contextual operational checklist from coverage and severity."""
    in_progress = [phase for phase, status in coverage.phases.items() if status == "in_progress"]
    items: list[str] = []

    for gap in coverage.gaps:
        items.append(PHASE_CHECKLIST_MAP.get(gap, f"Cover pending phase: {_phase_title(gap)}"))

    for phase in in_progress:
        items.append(f"Advance {_phase_title(phase)} from in_progress to done with deeper validation")

    high_count = sum(1 for event in events if event.severity == SeverityLevel.HIGH)
    med_count = sum(1 for event in events if event.severity == SeverityLevel.MED)
    if high_count > 0:
        items.append("Validate privilege escalation vectors")
        items.append("Check persistence mechanisms")
        items.append("Perform artifact cleanup validation")
    elif med_count >= 3:
        items.append("Re-test medium-severity findings to confirm exploitation impact")

    normalized_items = _normalize_unique(items)
    if not normalized_items:
        normalized_items = ["Maintain phase validation cadence and document residual risk assumptions"]

    return normalized_items, in_progress


def _build_risk_section(
    ai_analysis: dict[str, Any] | None,
    events: list[Event],
    coverage: CoverageSummaryRead,
) -> str:
    """Render risk section using AI output with deterministic fallback."""
    if ai_analysis:
        ai_risks = ai_analysis.get("risk_notes")
        if isinstance(ai_risks, list):
            ai_items = [str(item).strip() for item in ai_risks if str(item).strip()]
            if ai_items:
                return "\n".join([f"- {item}" for item in ai_items])

    notes = _deterministic_risk_notes(events, coverage)
    return "\n".join([f"- {item}" for item in notes])


def _build_recommendations_section(
    ai_analysis: dict[str, Any] | None,
    coverage: CoverageSummaryRead,
    checklist_items: list[str],
) -> str:
    """Render recommendations with AI priority and deterministic backup."""
    recommendation_pool: list[str] = []

    if ai_analysis:
        next_actions = ai_analysis.get("next_actions")
        if isinstance(next_actions, list):
            recommendation_pool.extend([str(item) for item in next_actions if str(item).strip()])

        attack_hypotheses = ai_analysis.get("attack_hypotheses")
        if isinstance(attack_hypotheses, list):
            recommendation_pool.extend(
                [f"Investigate hypothesis: {item}" for item in attack_hypotheses if str(item).strip()],
            )

    recommendation_pool.extend(coverage.recommendations)
    recommendation_pool.extend(checklist_items)

    recommendations = _normalize_unique(recommendation_pool)
    if not recommendations:
        recommendations = ["Continue execution across remaining phases and monitor for elevated-risk findings."]

    return "\n".join([f"- {item}" for item in recommendations])


def _build_appendix(events: list[Event]) -> str:
    """Render full event log appendix with size-safe truncation."""
    if not events:
        return "_No events recorded for this engagement._"

    lines: list[str] = []
    used_chars = 0
    for index, event in enumerate(events):
        line = (
            f"- [{event.id}] {_format_dt(event.ts)} | {event.phase.value} | {event.severity.value} "
            f"| {event.source} | {event.title} | {_summarize_content(event.content, MAX_APPENDIX_CONTENT_LEN)}"
        )
        projected = used_chars + len(line) + 1
        if lines and projected > MAX_APPENDIX_TOTAL_CHARS:
            remaining = len(events) - index
            lines.append(
                f"_Event log truncated safely: {remaining} additional event(s) omitted due to size constraints._",
            )
            break

        lines.append(line)
        used_chars = projected

    return "\n".join(lines)


def _default_vault_query(engagement_name: str) -> str:
    """Build default query to retrieve contextual note evidence."""
    return (
        f"{engagement_name} recon enumeration initial access privilege escalation "
        "post exploitation cleanup finding evidence notes"
    )


def _build_vault_evidence_section(hits: list[vault.VaultSearchHit]) -> str:
    """Render vault evidence bullets with source citations."""
    if not hits:
        return "_No matching note evidence found in the local vault._"

    lines: list[str] = []
    for item in hits[:MAX_VAULT_EVIDENCE_ITEMS]:
        excerpt = " ".join(re.sub(r"</?mark>", "", item.snippet).split())
        excerpt = _summarize_content(excerpt, max_len=MAX_VAULT_EVIDENCE_EXCERPT)
        lines.append(f"- {excerpt}")
        lines.append(f"  - Source: {item.file_path} (chunk {item.chunk_id})")
    return "\n".join(lines)


def _try_ai_analysis(
    db: Session,
    engagement_id: int,
    *,
    include_ai: bool | None = None,
) -> dict[str, Any] | None:
    """Run AI analysis and return payload, or None on provider/config/runtime failures."""
    ai_enabled = settings.ai_reports_enabled if include_ai is None else bool(include_ai)
    if not ai_enabled:
        return None

    try:
        return ai_analysis_service.analyze_engagement(db=db, engagement_id=engagement_id)
    except Exception:
        return None


def generate_report_payload(
    db: Session,
    engagement_id: int,
    *,
    include_vault: bool = False,
    include_ai: bool | None = None,
    vault_query: str | None = None,
) -> dict[str, Any]:
    """Generate consultancy-style markdown report and return structured payload."""
    engagement = get_engagement_by_id(db, engagement_id)
    if engagement is None:
        raise LookupError("engagement not found")

    generated_at = datetime.now(timezone.utc)
    events = _events_for_report(db, engagement_id)
    timeline_events = _timeline_for_report(db, engagement_id)
    coverage = coverage_engine.get_coverage(db, engagement_id)
    ai_analysis = _try_ai_analysis(
        db=db,
        engagement_id=engagement_id,
        include_ai=include_ai,
    )
    vault_hits: list[vault.VaultSearchHit] = []
    if include_vault:
        query = vault_query or _default_vault_query(engagement.name)
        try:
            vault_hits = vault.retrieve_relevant_chunks(
                db=db,
                query=query,
                limit=MAX_VAULT_EVIDENCE_ITEMS,
                explicit=True,
            )
        except Exception:
            vault_hits = []

    executive_summary = (
        str(ai_analysis.get("executive_summary", "")).strip()
        if ai_analysis
        else ""
    )
    if not executive_summary:
        executive_summary = _deterministic_executive_summary(engagement.name, events, coverage)

    technical_summary = (
        str(ai_analysis.get("technical_summary", "")).strip()
        if ai_analysis
        else ""
    )
    checklist_items, in_progress = _build_checklist(events, coverage)
    gap_text = ", ".join(coverage.gaps) if coverage.gaps else "none"
    in_progress_text = ", ".join(in_progress) if in_progress else "none"

    report_markdown = "\n".join(
        [
            f"# {settings.app_name} Security Report",
            "",
            "# Executive Summary",
            executive_summary,
            "",
            "# Engagement Overview",
            f"- Engagement ID: {engagement.id}",
            f"- Engagement Name: {engagement.name}",
            f"- Created At: {_format_dt(engagement.created_at)}",
            f"- Report Generated At: {_format_dt(generated_at)}",
            "",
            "# Scope",
            _build_scope_section(engagement.name, events),
            "",
            "# Methodology",
            _build_methodology_section(events),
            "",
            "# Timeline",
            _build_timeline_table(timeline_events),
            "",
            "# Coverage Analysis",
            _build_coverage_section(coverage),
            "",
            "# Technical Findings",
            _build_technical_findings(events, technical_summary=technical_summary),
            "",
            "# Operational Gaps & Checklist",
            f"- Coverage gaps: {gap_text}",
            f"- In-progress phases: {in_progress_text}",
            "",
            "## Checklist",
            "",
            *[f"- [ ] {item}" for item in checklist_items],
            "",
            "# Risk Assessment",
            _build_risk_section(ai_analysis=ai_analysis, events=events, coverage=coverage),
            "",
            "# Evidence from Notes",
            _build_vault_evidence_section(vault_hits),
            "",
            "# Recommendations",
            _build_recommendations_section(
                ai_analysis=ai_analysis,
                coverage=coverage,
                checklist_items=checklist_items,
            ),
            "",
            "# Appendix - Full Event Log",
            _build_appendix(events),
            "",
        ],
    )

    return {
        "engagement_id": engagement.id,
        "generated_at": generated_at,
        "report_markdown": report_markdown,
    }


def generate_report(
    db: Session,
    engagement_id: int,
    *,
    include_vault: bool = False,
    include_ai: bool | None = None,
    vault_query: str | None = None,
) -> str:
    """Legacy file-export path generation for backward compatibility."""
    payload = generate_report_payload(
        db=db,
        engagement_id=engagement_id,
        include_vault=include_vault,
        include_ai=include_ai,
        vault_query=vault_query,
    )

    reports_dir = _reports_dir()
    reports_dir.mkdir(parents=True, exist_ok=True)

    generated_at = payload["generated_at"]
    timestamp = generated_at.strftime("%Y%m%d_%H%M%S")
    filename = f"engagement_{engagement_id}_{timestamp}.md"
    report_path = reports_dir / filename
    report_path.write_text(payload["report_markdown"], encoding="utf-8")

    relative_path = Path("reports") / filename
    return relative_path.as_posix()
