"""Load system prompt files from resources/system-prompts/.

Caches loaded content in memory so each file is read at most once per
process lifetime.  The inline string literals in llm.py are kept as
fallbacks -- if a prompt file is missing, the loader returns None and
the caller falls through to the legacy inline constant.
"""

from __future__ import annotations

import logging
import re
from pathlib import Path
from typing import Optional

logger = logging.getLogger(__name__)

# resources/devsecscanner/backend/src/dsc_api/services/prompt_loader.py
#   parents: [0]=services [1]=dsc_api [2]=src [3]=backend [4]=devsecscanner [5]=resources
_PROMPTS_DIR = Path(__file__).resolve().parents[5] / "system-prompts"

# In-memory cache: path relative to _PROMPTS_DIR -> stripped content
_cache: dict[str, str] = {}

# ── Header-stripping regex ──────────────────────────────────────────
# Removes the top-level markdown title (# Title) and the immediately
# following blockquote metadata block (> Model: ..., > Version: ...).
# Everything after that is prompt content.
_HEADER_RE = re.compile(
    r"\A"
    r"#[^\n]*\n"        # # Title line
    r"(?:\s*\n)*"        # optional blank lines
    r"(?:>[^\n]*\n)*"    # blockquote metadata lines
    r"(?:\s*\n)*",       # trailing blank lines after metadata
)


def _load_file(rel_path: str) -> Optional[str]:
    """Load a prompt file, strip documentation headers, and cache."""
    if rel_path in _cache:
        return _cache[rel_path]

    path = _PROMPTS_DIR / rel_path
    if not path.is_file():
        logger.debug("Prompt file not found: %s", path)
        return None

    try:
        raw = path.read_text(encoding="utf-8")
    except OSError:
        logger.warning("Failed to read prompt file: %s", path)
        return None

    content = _HEADER_RE.sub("", raw).strip()
    _cache[rel_path] = content
    logger.info("Loaded prompt file: %s (%d chars)", rel_path, len(content))
    return content


# ── Public loaders for shared components ────────────────────────────

def load_identity() -> Optional[str]:
    """Load shared/identity.md (Deva personality + tone rules)."""
    return _load_file("shared/identity.md")


def load_base_rules() -> Optional[str]:
    """Load shared/base-rules.md (always-on operational rules)."""
    return _load_file("shared/base-rules.md")


def load_cwe_disambiguation() -> Optional[str]:
    """Load shared/cwe-disambiguation.md (CWE classification guide)."""
    return _load_file("shared/cwe-disambiguation.md")


def load_compliance_mappings() -> Optional[str]:
    """Load shared/compliance-mappings.md (CWE-to-control for 9 frameworks)."""
    return _load_file("shared/compliance-mappings.md")


# ── Model-specific prompt resolution ───────────────────────────────
#
# Maps model IDs from DEVSEC_MODEL_ENTRIES to prompt file paths.
# Exact match first, then prefix patterns, then provider fallback.

_MODEL_ID_TO_FILE: dict[str, str] = {
    # Claude
    "claude-opus-4-7": "frontier/claude/opus-4.7.md",
    "claude-opus-4-6": "frontier/claude/opus-4.6.md",
    "claude-sonnet-4-6": "frontier/claude/sonnet-4.6.md",
    # OpenAI
    "gpt-4.1": "frontier/openai/gpt-4.1.md",
    "o4-mini": "frontier/openai/o4-mini.md",
    # Gemini
    "gemini-cli": "frontier/gemini/cli.md",
    # Deva domain models
    "deva-coder": "deva-models/deva-coder/v8.md",
    "deva-hospital": "deva-models/deva-hospital/v16.md",
    "deva-finance": "deva-models/deva-finance/v1.md",
    "deva-detector": "deva-models/deva-detector/v17.md",
}

# Prefix patterns: checked in order after exact match fails.
# Tuple of (prefix, file_path).
_MODEL_PREFIX_PATTERNS: list[tuple[str, str]] = [
    ("claude-haiku-4-5", "frontier/claude/haiku-4.5.md"),
    ("claude-sonnet-4", "frontier/claude/sonnet-4.6.md"),
    ("claude-opus-4", "frontier/claude/opus-4.7.md"),
    ("gpt-5", "frontier/openai/gpt-4.1.md"),  # fallback to GPT-4.1 patterns
    ("gpt-4.1", "frontier/openai/gpt-4.1.md"),
    ("gpt-4o", "frontier/openai/gpt-4o.md"),
    ("o4-", "frontier/openai/o4-mini.md"),
    ("o3-", "frontier/openai/o4-mini.md"),
    ("gemini-3.1-pro", "frontier/gemini/pro-3.1.md"),
    ("gemini-2.5-pro", "frontier/gemini/pro-2.5.md"),
    ("gemini-3.1-flash", "frontier/gemini/flash-2.5.md"),
    ("gemini-2.5-flash", "frontier/gemini/flash-2.5.md"),
    ("gemma-4", "frontier/gemini/pro-3.1.md"),
    ("gemma4", "frontier/gemini/pro-3.1.md"),
]

# Provider fallbacks: used when no exact or prefix match hits.
_PROVIDER_FALLBACKS: dict[str, str] = {
    "claude": "frontier/claude/opus-4.7.md",
    "openai": "frontier/openai/gpt-4.1.md",
    "gemini": "frontier/gemini/pro-3.1.md",
    "ollama": "frontier/ollama/generic.md",
}


def _resolve_provider(model_id: str) -> Optional[str]:
    """Infer provider from model ID for fallback resolution."""
    mid = model_id.lower()
    if mid.startswith("claude-") or "anthropic" in mid:
        return "claude"
    if mid.startswith("gpt-") or mid.startswith("o3-") or mid.startswith("o4-") or "codex" in mid:
        return "openai"
    if mid.startswith("gemini-") or mid.startswith("gemma-") or mid.startswith("gemma4"):
        return "gemini"
    # Most remaining models (qwen, deepseek, phi, llama, mistral) run via Ollama
    return "ollama"


def load_model_prompt(model_id: str) -> Optional[str]:
    """Load the model-specific prompt for the given model ID.

    Resolution order:
    1. Exact match in _MODEL_ID_TO_FILE
    2. Prefix match in _MODEL_PREFIX_PATTERNS
    3. Provider-level fallback
    4. None (caller uses inline fallback)
    """
    if not model_id:
        return None

    mid = model_id.lower().strip()

    # 1. Exact match
    if mid in _MODEL_ID_TO_FILE:
        return _load_file(_MODEL_ID_TO_FILE[mid])

    # 2. Prefix match
    for prefix, file_path in _MODEL_PREFIX_PATTERNS:
        if mid.startswith(prefix):
            return _load_file(file_path)

    # 3. Provider fallback
    provider = _resolve_provider(mid)
    if provider and provider in _PROVIDER_FALLBACKS:
        return _load_file(_PROVIDER_FALLBACKS[provider])

    return None


def is_deva_domain_model(model_id: str) -> bool:
    """True if the model ID is a Deva domain-specific model."""
    return model_id.lower().strip() in {
        "deva-coder", "deva-hospital", "deva-finance", "deva-detector",
    }


# ── Surface prompt loading ──────────────────────────────────────────

_SURFACE_FILES: dict[str, str] = {
    "task-planner": "surfaces/task-planner.md",
    "inline-edit": "surfaces/inline-edit.md",
    "security-auditor": "surfaces/security-auditor.md",
    "remote-mode": "surfaces/remote-mode.md",
}


def load_surface_prompt(surface: str) -> Optional[str]:
    """Load a surface-specific prompt by name."""
    file_path = _SURFACE_FILES.get(surface)
    if not file_path:
        return None
    return _load_file(file_path)


# ── Cache management ────────────────────────────────────────────────

def clear_cache() -> None:
    """Clear the in-memory prompt cache. Useful for dev/reload."""
    _cache.clear()
    logger.info("Prompt cache cleared")
