"""Structured notes markdown generator."""

from __future__ import annotations

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

from sqlalchemy import select
from sqlalchemy.orm import Session

from app.core.config import settings
from app.models.event import Event, EventPhase
from app.services.engagements import get_engagement_by_id
from app.services import vault

MAX_CONTENT_LEN = 500
MAX_IMPORTED_VAULT_ITEMS = 12
MAX_IMPORTED_VAULT_EXCERPT = 260
PHASE_TITLES = {
    EventPhase.RECON: "Recon",
    EventPhase.ENUMERATION: "Enumeration",
    EventPhase.INITIAL_ACCESS: "Initial Access",
    EventPhase.PRIV_ESC: "Privilege Escalation",
    EventPhase.POST_EXPLOITATION: "Post Exploitation",
    EventPhase.CLEANUP: "Cleanup",
}


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


def _notes_dir() -> Path:
    """Return notes directory path."""
    return _project_root() / "notes"


def _format_dt(value: datetime) -> str:
    """Format timestamps for consistent markdown 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 _summarize_content(content: str, max_len: int = MAX_CONTENT_LEN) -> str:
    """Trim long event content to readable length."""
    text = " ".join(content.split())
    if len(text) <= max_len:
        return text
    return f"{text[: max_len - 3]}..."


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


def _group_events(events: list[Event]) -> dict[EventPhase, list[Event]]:
    """Group events by phase preserving phase order."""
    grouped: dict[EventPhase, list[Event]] = {phase: [] for phase in EventPhase}
    for event in events:
        grouped[event.phase].append(event)
    return grouped


def _render_phase_events(phase_events: list[Event]) -> str:
    """Render markdown block for one phase."""
    if not phase_events:
        return "_No events recorded._"

    lines: list[str] = []
    for event in phase_events:
        lines.extend(
            [
                f"- Timestamp: {_format_dt(event.ts)}",
                f"- Title: {event.title}",
                f"- Source: {event.source}",
                f"- Content: {_summarize_content(event.content)}",
                "",
            ],
        )
    return "\n".join(lines).strip()


def _render_phases(grouped_events: dict[EventPhase, list[Event]]) -> str:
    """Render all phase sections in consistent order."""
    lines: list[str] = ["## Phases", ""]
    for phase in EventPhase:
        lines.extend(
            [
                f"### {PHASE_TITLES[phase]}",
                _render_phase_events(grouped_events[phase]),
                "",
            ],
        )
    return "\n".join(lines).strip()


def _build_incremental_section(
    generated_at: datetime,
    grouped_events: dict[EventPhase, list[Event]],
    imported_section: str | None = None,
) -> str:
    """Build incremental append block for existing notes file."""
    blocks = [
        "",
        "---",
        "",
        f"## {settings.app_name} Incremental Update ({_format_dt(generated_at)})",
        "",
        _render_phases(grouped_events),
        "",
    ]
    if imported_section:
        blocks.extend([imported_section, ""])
    return "\n".join(blocks)


def _build_initial_document_with_vault(
    engagement_name: str,
    grouped_events: dict[EventPhase, list[Event]],
    imported_section: str | None,
) -> str:
    """Build initial notes doc with optional imported vault section."""
    blocks = [
        f"# {settings.app_name} Engagement Notes",
        "",
        f"## Engagement: {engagement_name}",
        "",
        "## Overview",
        "Automatically generated engagement notes with phase-by-phase event tracking.",
        "",
        _render_phases(grouped_events),
        "",
    ]
    if imported_section:
        blocks.extend([imported_section, ""])
    return "\n".join(blocks)


def _default_vault_query(engagement_name: str) -> str:
    """Build default query used for importing vault snippets."""
    return (
        f"{engagement_name} recon enumeration initial access privilege escalation "
        "post exploitation cleanup findings notes"
    )


def _build_imported_vault_section(hits: list[vault.VaultSearchHit]) -> str | None:
    """Render imported snippets grouped by source file."""
    if not hits:
        return None

    grouped: dict[str, list[str]] = {}
    for item in hits[:MAX_IMPORTED_VAULT_ITEMS]:
        excerpt = " ".join(re.sub(r"</?mark>", "", item.snippet).split())
        if len(excerpt) > MAX_IMPORTED_VAULT_EXCERPT:
            excerpt = f"{excerpt[: MAX_IMPORTED_VAULT_EXCERPT - 3]}..."
        grouped.setdefault(item.file_path, []).append(excerpt)

    lines: list[str] = ["## Imported Vault Notes", ""]
    for file_path, excerpts in grouped.items():
        lines.append(f"### {file_path}")
        for excerpt in excerpts:
            lines.append(f"- {excerpt}")
        lines.append("")
    return "\n".join(lines).strip()


def generate_notes(
    db: Session,
    engagement_id: int,
    *,
    include_vault: bool = False,
    vault_query: str | None = None,
) -> str:
    """Generate or append notes markdown for one engagement."""
    engagement = get_engagement_by_id(db, engagement_id)
    if engagement is None:
        raise LookupError("engagement not found")

    notes_dir = _notes_dir()
    notes_dir.mkdir(parents=True, exist_ok=True)

    note_path = notes_dir / f"engagement_{engagement_id}.md"
    events = _events_for_engagement(db, engagement_id)
    grouped_events = _group_events(events)
    generated_at = datetime.now(timezone.utc)

    imported_section: str | None = None
    if include_vault:
        query = vault_query or _default_vault_query(engagement.name)
        try:
            hits = vault.retrieve_relevant_chunks(
                db=db,
                query=query,
                limit=MAX_IMPORTED_VAULT_ITEMS,
                explicit=True,
            )
            imported_section = _build_imported_vault_section(hits)
        except Exception:
            imported_section = None

    if note_path.exists():
        append_content = _build_incremental_section(
            generated_at,
            grouped_events,
            imported_section=imported_section,
        )
        with note_path.open("a", encoding="utf-8") as fp:
            fp.write(append_content)
    else:
        note_path.write_text(
            _build_initial_document_with_vault(
                engagement.name,
                grouped_events,
                imported_section=imported_section,
            ),
            encoding="utf-8",
        )

    return (Path("notes") / note_path.name).as_posix()
