"""Large-file interception pipeline for Hippo LCM.

Ports the TypeScript large-files.ts module: file-block parsing, file-ID extraction,
structured/code/text exploration, and exploration summary generation.
"""

from __future__ import annotations

import json
import re
from dataclasses import dataclass, field
from typing import Awaitable, Callable, Optional, Protocol

# ---------------------------------------------------------------------------
# Regex patterns
# ---------------------------------------------------------------------------

FILE_BLOCK_RE = re.compile(r"<file\b([^>]*)>([\s\S]*?)</file>", re.IGNORECASE)
FILE_ID_RE = re.compile(r"\bfile_[a-f0-9]{16}\b", re.IGNORECASE)

# ---------------------------------------------------------------------------
# Extension / MIME constants
# ---------------------------------------------------------------------------

CODE_EXTENSIONS: set[str] = {
    "c", "cc", "cpp", "cs", "go", "h", "hpp", "java", "js", "jsx",
    "kt", "m", "php", "py", "rb", "rs", "scala", "sh", "sql", "swift",
    "ts", "tsx",
}

STRUCTURED_EXTENSIONS: set[str] = {"csv", "json", "tsv", "xml", "yaml", "yml"}

MIME_EXTENSION_MAP: dict[str, str] = {
    "application/json": "json",
    "application/xml": "xml",
    "application/yaml": "yaml",
    "application/x-yaml": "yaml",
    "application/x-ndjson": "json",
    "application/csv": "csv",
    "application/javascript": "js",
    "application/typescript": "ts",
    "application/x-python-code": "py",
    "application/x-rust": "rs",
    "application/x-sh": "sh",
    "text/csv": "csv",
    "text/markdown": "md",
    "text/plain": "txt",
    "text/tab-separated-values": "tsv",
    "text/x-c": "c",
    "text/x-c++": "cpp",
    "text/x-go": "go",
    "text/x-java": "java",
    "text/x-python": "py",
    "text/x-rust": "rs",
    "text/x-script.python": "py",
    "text/x-shellscript": "sh",
    "text/x-typescript": "ts",
    "text/xml": "xml",
}

STRUCTURED_MIME_PREFIXES: list[str] = [
    "application/json",
    "application/xml",
    "application/yaml",
    "application/x-yaml",
    "application/x-ndjson",
    "text/csv",
    "text/tab-separated-values",
    "text/xml",
]

CODE_MIME_PREFIXES: list[str] = [
    "application/javascript",
    "application/typescript",
    "application/x-python-code",
    "application/x-rust",
    "text/javascript",
    "text/x-c",
    "text/x-c++",
    "text/x-go",
    "text/x-java",
    "text/x-python",
    "text/x-rust",
    "text/x-script.python",
    "text/x-shellscript",
    "text/x-typescript",
]

TEXT_SUMMARY_SLICE_CHARS = 2_400
TEXT_HEADER_LIMIT = 18

# ---------------------------------------------------------------------------
# Data classes
# ---------------------------------------------------------------------------


@dataclass
class FileBlock:
    full_match: str
    start: int
    end: int
    attributes: dict[str, str] = field(default_factory=dict)
    file_name: Optional[str] = None
    mime_type: Optional[str] = None
    text: str = ""


@dataclass
class ExplorationSummaryInput:
    content: str
    file_name: Optional[str] = None
    mime_type: Optional[str] = None
    summarize_text: Optional[Callable[[str], Awaitable[Optional[str]]]] = None


# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------

_ATTR_RE = re.compile(
    r"""([A-Za-z_:][A-Za-z0-9_:.\-]*)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>]+))"""
)


def _parse_file_attributes(raw: str) -> dict[str, str]:
    attrs: dict[str, str] = {}
    for m in _ATTR_RE.finditer(raw):
        key = m.group(1).strip().lower()
        value = (m.group(2) if m.group(2) is not None
                 else m.group(3) if m.group(3) is not None
                 else m.group(4) or "").strip()
        if key and value:
            attrs[key] = value
    return attrs


def _normalize_text_for_line(text: str, max_len: int) -> str:
    compact = re.sub(r"\s+", " ", text).strip()
    if len(compact) <= max_len:
        return compact
    return compact[:max_len] + "..."


def _collect_file_name_extension(file_name: Optional[str]) -> Optional[str]:
    if not file_name:
        return None
    base = file_name.strip().replace("\\", "/").split("/")[-1]
    idx = base.rfind(".")
    if idx <= 0 or idx == len(base) - 1:
        return None
    ext = base[idx + 1:].lower()
    if not re.fullmatch(r"[a-z0-9]{1,10}", ext):
        return None
    return ext


def _guess_mime_extension(mime_type: Optional[str]) -> Optional[str]:
    if not mime_type:
        return None
    normalized = mime_type.strip().lower()
    return MIME_EXTENSION_MAP.get(normalized)


def is_structured(*, mime_type: Optional[str] = None, extension: Optional[str] = None) -> bool:
    if mime_type:
        mime_lower = mime_type.strip().lower()
        if any(mime_lower.startswith(prefix) for prefix in STRUCTURED_MIME_PREFIXES):
            return True
    return extension in STRUCTURED_EXTENSIONS if extension else False


def is_code(*, mime_type: Optional[str] = None, extension: Optional[str] = None) -> bool:
    if mime_type:
        mime_lower = mime_type.strip().lower()
        if any(mime_lower.startswith(prefix) for prefix in CODE_MIME_PREFIXES):
            return True
    return extension in CODE_EXTENSIONS if extension else False


def _unique_ordered(values: list[str]) -> list[str]:
    seen: set[str] = set()
    out: list[str] = []
    for v in values:
        if v not in seen:
            seen.add(v)
            out.append(v)
    return out


# ---------------------------------------------------------------------------
# Structured data exploration
# ---------------------------------------------------------------------------


def _explore_json(content: str) -> str:
    parsed = json.loads(content)

    def describe(value: object, depth: int = 0) -> str:
        if depth >= 2:
            return "..."
        if isinstance(value, list):
            sample = [describe(item, depth + 1) for item in value[:3]]
            sample_str = f", sample=[{', '.join(sample)}]" if sample else ""
            return f"array(len={len(value)}{sample_str})"
        if isinstance(value, dict):
            keys = list(value.keys())
            preview = ", ".join(keys[:10])
            return f"object(keys={len(keys)}{': ' + preview if preview else ''})"
        if value is None:
            return "null"
        if isinstance(value, bool):
            return "boolean"
        if isinstance(value, (int, float)):
            return "number"
        if isinstance(value, str):
            return "string"
        return type(value).__name__

    top_level = "array" if isinstance(parsed, list) else type(parsed).__name__
    # Match TS: typeof null == "object", typeof string == "string", etc.
    if parsed is None:
        top_level = "object"
    elif isinstance(parsed, bool):
        top_level = "boolean"
    elif isinstance(parsed, (int, float)):
        top_level = "number"
    elif isinstance(parsed, str):
        top_level = "string"
    elif isinstance(parsed, dict):
        top_level = "object"

    return "\n".join([
        "Structured summary (JSON):",
        f"Top-level type: {top_level}.",
        f"Shape: {describe(parsed)}.",
    ])


def _parse_delimited_line(line: str, delimiter: str) -> list[str]:
    return [item.strip() for item in line.split(delimiter) if item.strip()]


def _explore_delimited(content: str, delimiter: str, kind: str) -> str:
    lines = [line.strip() for line in content.split("\n") if line.strip()]
    # Handle \r\n
    lines = [line.strip() for line in re.split(r"\r?\n", content) if line.strip()]

    if not lines:
        return f"Structured summary ({kind}): no rows found."

    headers = _parse_delimited_line(lines[0], delimiter)
    row_count = max(0, len(lines) - 1)
    first_data = _normalize_text_for_line(lines[1], 180) if len(lines) > 1 else "(no data rows)"

    return "\n".join([
        f"Structured summary ({kind}):",
        f"Rows: {row_count:,}.",
        f"Columns ({len(headers)}): {', '.join(headers) or '(none detected)'}.",
        f"First row sample: {first_data}.",
    ])


def _explore_yaml(content: str) -> str:
    top_level_keys = _unique_ordered([
        m.group(1)
        for line in re.split(r"\r?\n", content)
        for m in [re.match(r"^([A-Za-z0-9_.\-]+):\s*(?:#.*)?$", line)]
        if m
    ])

    return "\n".join([
        "Structured summary (YAML):",
        f"Top-level keys ({len(top_level_keys)}): "
        f"{', '.join(top_level_keys[:30]) or '(none detected)'}.",
    ])


def _explore_xml(content: str) -> str:
    root_match = re.search(r"<([A-Za-z0-9_:\-]+)(\s|>)", content)
    root_tag = root_match.group(1) if root_match else "unknown"
    child_tags = _unique_ordered([
        m.group(1)
        for m in re.finditer(r"<([A-Za-z0-9_:\-]+)(\s|>)", content)
        if m.group(1) != root_tag
    ][:30])

    return "\n".join([
        "Structured summary (XML):",
        f"Root element: {root_tag}.",
        f"Child elements seen: {', '.join(child_tags) or '(none detected)'}.",
    ])


def explore_structured_data(
    content: str,
    mime_type: Optional[str] = None,
    file_name: Optional[str] = None,
) -> str:
    extension = _collect_file_name_extension(file_name) or _guess_mime_extension(mime_type)
    normalized_mime = (mime_type.strip().lower() if mime_type else "")

    if extension == "json" or normalized_mime.startswith("application/json"):
        try:
            return _explore_json(content)
        except (json.JSONDecodeError, ValueError):
            return "Structured summary (JSON): failed to parse as valid JSON."

    if extension == "csv" or normalized_mime.startswith("text/csv"):
        return _explore_delimited(content, ",", "CSV")

    if extension == "tsv" or normalized_mime.startswith("text/tab-separated-values"):
        return _explore_delimited(content, "\t", "TSV")

    if (
        extension == "xml"
        or normalized_mime.startswith("text/xml")
        or normalized_mime.startswith("application/xml")
    ):
        return _explore_xml(content)

    if extension in ("yaml", "yml") or "yaml" in normalized_mime:
        return _explore_yaml(content)

    return "\n".join([
        "Structured summary:",
        f"Characters: {len(content):,}.",
        f"Lines: {len(re.split(chr(10), content)):,}.",
    ])


# ---------------------------------------------------------------------------
# Code exploration
# ---------------------------------------------------------------------------

_IMPORT_RE = re.compile(
    r"^\s*(import\s+|from\s+\S+\s+import\s+|const\s+\w+\s*=\s*require\()"
)

_SIGNATURE_RE = re.compile(
    r"^(export\s+)?(async\s+)?"
    r"(function|class|interface|type|const\s+\w+\s*=\s*\(|def\s+\w+\(|struct\s+\w+)"
)


def explore_code(content: str, file_name: Optional[str] = None) -> str:
    lines = content.split("\n")
    # Handle \r\n properly
    lines = re.split(r"\r?\n", content)

    imports = _unique_ordered([
        _normalize_text_for_line(line, 180)
        for line in lines
        if _IMPORT_RE.match(line)
    ][:12])

    signatures = _unique_ordered([
        _normalize_text_for_line(line.strip(), 200)
        for line in lines
        if _SIGNATURE_RE.match(line.strip())
    ][:24])

    return "\n".join([
        f"Code exploration summary{f' ({file_name})' if file_name else ''}:",
        f"Lines: {len(lines):,}.",
        f"Imports/dependencies ({len(imports)}): {' | '.join(imports) or 'none detected'}.",
        f"Top-level definitions ({len(signatures)}): {' | '.join(signatures) or 'none detected'}.",
    ])


# ---------------------------------------------------------------------------
# Text exploration
# ---------------------------------------------------------------------------


def _extract_text_headers(content: str) -> list[str]:
    return _unique_ordered([
        _normalize_text_for_line(line.strip(), 160)
        for line in re.split(r"\r?\n", content)
        if line.strip() and len(line.strip()) > 1
        and (re.match(r"^#{1,6}\s+", line.strip())
             or re.fullmatch(r"[A-Z0-9][A-Z0-9\s:_\-]{6,}", line.strip()))
    ][:TEXT_HEADER_LIMIT])


def _build_text_sample(content: str) -> str:
    if len(content) <= TEXT_SUMMARY_SLICE_CHARS * 2:
        return content

    middle_start = max(0, len(content) // 2 - TEXT_SUMMARY_SLICE_CHARS // 2)
    middle_end = middle_start + TEXT_SUMMARY_SLICE_CHARS
    head = content[:TEXT_SUMMARY_SLICE_CHARS]
    mid = content[middle_start:middle_end]
    tail = content[-TEXT_SUMMARY_SLICE_CHARS:]

    return "\n\n".join([
        "[Document Start]", head,
        "[Document Middle]", mid,
        "[Document End]", tail,
    ])


def _build_text_prompt(
    content: str,
    headers: list[str],
    file_name: Optional[str] = None,
    mime_type: Optional[str] = None,
) -> str:
    sample = _build_text_sample(content)
    line_count = len(re.split(r"\r?\n", content))
    headers_str = (" | ".join(headers)) if headers else "none"

    return "\n".join([
        "Summarize this large file for retrieval-time context references.",
        f"File name: {file_name or 'unknown'}",
        f"Mime type: {mime_type or 'unknown'}",
        f"Length: {len(content):,} chars",
        f"Line count: {line_count:,}",
        f"Detected section headers: {headers_str}",
        "Produce 200-300 words with:",
        "- What the document is about",
        "- Key sections and topics",
        "- Important names, dates, and numbers",
        "- Any action items or constraints",
        "Do not quote long passages verbatim.",
        "",
        "Document sample:",
        sample,
    ])


def _explore_text_deterministic_fallback(
    content: str, file_name: Optional[str] = None
) -> str:
    normalized = re.sub(r"\s+", " ", content).strip()
    headers = _extract_text_headers(content)
    line_count = len(re.split(r"\r?\n", content))
    word_count = len(normalized.split()) if normalized else 0
    first = _normalize_text_for_line(content[:500], 500)
    last = _normalize_text_for_line(content[-500:], 500)

    return "\n".join([
        f"Text exploration summary{f' ({file_name})' if file_name else ''}:",
        f"Characters: {len(content):,}.",
        f"Words: {word_count:,}.",
        f"Lines: {line_count:,}.",
        f"Detected section headers: {' | '.join(headers) or 'none detected'}.",
        f"Opening excerpt: {first or '(empty)'}.",
        f"Closing excerpt: {last or '(empty)'}.",
    ])


async def explore_text(input: ExplorationSummaryInput) -> str:
    headers = _extract_text_headers(input.content)

    if input.summarize_text is not None:
        prompt = _build_text_prompt(
            input.content,
            headers,
            file_name=input.file_name,
            mime_type=input.mime_type,
        )
        try:
            summary = await input.summarize_text(prompt)
            if isinstance(summary, str) and summary.strip():
                return summary.strip()
        except Exception:
            pass  # Fall through to deterministic fallback

    return _explore_text_deterministic_fallback(input.content, input.file_name)


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------


def parse_file_blocks(content: str) -> list[FileBlock]:
    blocks: list[FileBlock] = []
    for m in FILE_BLOCK_RE.finditer(content):
        full_match = m.group(0)
        raw_attrs = m.group(1) or ""
        text = m.group(2) or ""
        start = m.start()
        end = m.end()
        attributes = _parse_file_attributes(raw_attrs)

        blocks.append(FileBlock(
            full_match=full_match,
            start=start,
            end=end,
            attributes=attributes,
            file_name=attributes.get("name"),
            mime_type=attributes.get("mime"),
            text=text,
        ))
    return blocks


def extension_from_name_or_mime(
    file_name: Optional[str] = None,
    mime_type: Optional[str] = None,
) -> str:
    from_name = _collect_file_name_extension(file_name)
    if from_name:
        return from_name

    from_mime = _guess_mime_extension(mime_type)
    if from_mime:
        return from_mime

    return "txt"


def extract_file_ids_from_content(content: str) -> list[str]:
    matches = FILE_ID_RE.findall(content)
    return _unique_ordered([m.lower() for m in matches])


def format_file_reference(
    file_id: str,
    byte_size: int,
    summary: str,
    file_name: Optional[str] = None,
    mime_type: Optional[str] = None,
) -> str:
    name = (file_name.strip() if file_name and file_name.strip() else "unknown")
    mime = (mime_type.strip() if mime_type and mime_type.strip() else "unknown")
    safe_size = max(0, byte_size)

    return "\n".join([
        f"[LCM File: {file_id} | {name} | {mime} | {safe_size:,} bytes]",
        "",
        "Exploration Summary:",
        summary.strip() or "(no summary available)",
    ])


async def generate_exploration_summary(input: ExplorationSummaryInput) -> str:
    extension = extension_from_name_or_mime(input.file_name, input.mime_type)

    if is_structured(mime_type=input.mime_type, extension=extension):
        return explore_structured_data(input.content, input.mime_type, input.file_name)

    if is_code(mime_type=input.mime_type, extension=extension):
        return explore_code(input.content, input.file_name)

    return await explore_text(input)
