"""Multi-format parsers for KB ingest.

Converts various file formats into markdown that the standard ingest pipeline can process.
Each parser takes raw file content and returns structured markdown.

Supported formats:
- PDF documents (.pdf) — full text extraction via pdfplumber/PyPDF2
- Claude Code history (.jsonl from ~/.claude/projects/)
- ChatGPT export (.json from Settings → Data controls → Export)
- Slack export (.json per channel)
- Meeting transcripts (.vtt, .srt)
- Generic JSON/JSONL (auto-detected structure)
"""

from __future__ import annotations

import json
import logging
from datetime import datetime
from pathlib import Path

log = logging.getLogger(__name__)


def parse_file(filepath: Path) -> str | None:
    """Auto-detect format and parse to markdown. Returns None if not a parseable format."""
    ext = filepath.suffix.lower()

    # PDF — binary format, handle before text read
    if ext == ".pdf":
        return parse_pdf(filepath)

    try:
        raw = filepath.read_text(encoding="utf-8", errors="replace")
    except Exception:
        return None

    # Claude Code history (JSONL with role/content)
    if ext == ".jsonl":
        return _parse_claude_history(raw, filepath.name)

    # ChatGPT export (single JSON with conversations array)
    if ext == ".json" and ("conversations" in raw[:500] or '"mapping"' in raw[:1000]):
        return _parse_chatgpt_export(raw, filepath.name)

    # Slack export (JSON array with messages)
    if ext == ".json" and ('"type":"message"' in raw[:1000] or '"subtype"' in raw[:500]):
        return _parse_slack_export(raw, filepath.name)

    # Generic JSON
    if ext == ".json":
        return _parse_generic_json(raw, filepath.name)

    # Meeting transcripts
    if ext in (".vtt", ".srt"):
        return _parse_transcript(raw, filepath.name, ext)

    # JSONL (generic)
    if ext == ".jsonl":
        return _parse_generic_jsonl(raw, filepath.name)

    return None


def parse_pdf(filepath: Path) -> str | None:
    """Extract full text from a PDF file.

    Tries pdfplumber first (better table/layout handling), falls back to PyPDF2.
    Returns the complete text content — no truncation here; the caller decides
    how to handle large content (chunked multi-pass extraction).
    """
    try:
        import pdfplumber
        with pdfplumber.open(filepath) as pdf:
            pages: list[str] = []
            for i, page in enumerate(pdf.pages):
                text = page.extract_text() or ""
                if text.strip():
                    pages.append(f"[Page {i + 1}]\n{text}")
            if pages:
                full_text = "\n\n".join(pages)
                log.info("PDF parsed via pdfplumber: %s (%d pages, %d chars)",
                         filepath.name, len(pdf.pages), len(full_text))
                return full_text
            # All pages empty — likely a scanned/image PDF
            log.warning("PDF %s: all pages empty (scanned PDF?) — %d pages",
                        filepath.name, len(pdf.pages))
            return f"[Scanned PDF: {filepath.name} — {len(pdf.pages)} pages, no extractable text]"
    except ImportError:
        pass
    except Exception:
        log.debug("pdfplumber failed for %s", filepath.name, exc_info=True)

    # Fallback: PyPDF2
    try:
        from PyPDF2 import PdfReader
        reader = PdfReader(str(filepath))
        pages = []
        for i, page in enumerate(reader.pages):
            text = page.extract_text() or ""
            if text.strip():
                pages.append(f"[Page {i + 1}]\n{text}")
        if pages:
            full_text = "\n\n".join(pages)
            log.info("PDF parsed via PyPDF2: %s (%d pages, %d chars)",
                     filepath.name, len(reader.pages), len(full_text))
            return full_text
        log.warning("PDF %s: all pages empty via PyPDF2", filepath.name)
        return f"[Scanned PDF: {filepath.name} — {len(reader.pages)} pages, no extractable text]"
    except ImportError:
        log.warning("No PDF library available — install pdfplumber: pip install pdfplumber")
        return f"[PDF document: {filepath.name} — install pdfplumber for text extraction]"
    except Exception:
        log.exception("PDF extraction failed for %s", filepath.name)
        return None


def _parse_claude_history(raw: str, filename: str) -> str:
    """Parse Claude Code JSONL conversation history."""
    lines = raw.strip().split("\n")
    messages = []
    for line in lines:
        try:
            msg = json.loads(line)
        except json.JSONDecodeError:
            continue
        role = msg.get("role", msg.get("type", ""))
        content = msg.get("content", "")
        if isinstance(content, list):
            # Anthropic format: content is array of blocks
            text_parts = []
            for block in content:
                if isinstance(block, dict) and block.get("type") == "text":
                    text_parts.append(block.get("text", ""))
                elif isinstance(block, str):
                    text_parts.append(block)
            content = "\n".join(text_parts)
        if content and role:
            messages.append((role, content[:2000]))

    if not messages:
        return f"# Claude History: {filename}\n\n[Empty or unparseable JSONL file]"

    md = f"# Claude Code Conversation: {filename}\n\n"
    md += f"**Messages:** {len(messages)}\n\n"

    for role, content in messages[:50]:  # cap at 50 messages
        label = "**Human**" if role in ("user", "human") else "**Assistant**" if role == "assistant" else f"**{role}**"
        md += f"{label}:\n{content[:1000]}\n\n---\n\n"

    if len(messages) > 50:
        md += f"\n*({len(messages) - 50} more messages truncated)*\n"

    return md


def _parse_chatgpt_export(raw: str, filename: str) -> str:
    """Parse ChatGPT data export (Settings → Export my data)."""
    try:
        data = json.loads(raw)
    except json.JSONDecodeError:
        return f"# ChatGPT Export: {filename}\n\n[Invalid JSON]"

    conversations = data if isinstance(data, list) else data.get("conversations", [])
    if not conversations:
        return f"# ChatGPT Export: {filename}\n\n[No conversations found]"

    md = f"# ChatGPT Export: {filename}\n\n"
    md += f"**Conversations:** {len(conversations)}\n\n"

    for conv in conversations[:20]:  # cap
        title = conv.get("title", "Untitled")
        created = conv.get("create_time", "")
        if created and isinstance(created, (int, float)):
            created = datetime.fromtimestamp(created).strftime("%Y-%m-%d %H:%M")

        md += f"## {title}\n"
        if created:
            md += f"*{created}*\n\n"

        # ChatGPT uses a mapping structure
        mapping = conv.get("mapping", {})
        if mapping:
            sorted_nodes = sorted(mapping.values(), key=lambda n: n.get("message", {}).get("create_time", 0) if n.get("message") else 0)
            for node in sorted_nodes[:30]:
                msg = node.get("message")
                if not msg:
                    continue
                role = msg.get("author", {}).get("role", "")
                parts = msg.get("content", {}).get("parts", [])
                text = "\n".join(str(p) for p in parts if isinstance(p, str))
                if text and role in ("user", "assistant"):
                    label = "**Human**" if role == "user" else "**Assistant**"
                    md += f"{label}: {text[:500]}\n\n"
        md += "---\n\n"

    return md


def _parse_slack_export(raw: str, filename: str) -> str:
    """Parse Slack channel export JSON."""
    try:
        messages = json.loads(raw)
    except json.JSONDecodeError:
        return f"# Slack Export: {filename}\n\n[Invalid JSON]"

    if not isinstance(messages, list):
        return f"# Slack Export: {filename}\n\n[Expected array of messages]"

    md = f"# Slack Channel: {filename}\n\n"
    md += f"**Messages:** {len(messages)}\n\n"

    for msg in messages[:100]:
        user = msg.get("user_profile", {}).get("real_name", msg.get("user", "Unknown"))
        text = msg.get("text", "")
        ts = msg.get("ts", "")
        if ts:
            try:
                ts = datetime.fromtimestamp(float(ts)).strftime("%Y-%m-%d %H:%M")
            except (ValueError, OSError):
                pass

        if text:
            md += f"**{user}** ({ts}): {text[:500]}\n\n"

    return md


def _parse_transcript(raw: str, filename: str, ext: str) -> str:
    """Parse VTT or SRT meeting transcripts."""
    md = f"# Meeting Transcript: {filename}\n\n"

    if ext == ".vtt":
        # WebVTT format
        blocks = raw.split("\n\n")
        for block in blocks:
            lines = block.strip().split("\n")
            # Skip header and timing lines
            text_lines = [l for l in lines if not l.startswith("WEBVTT") and "-->" not in l and l.strip()]
            if text_lines:
                md += " ".join(text_lines) + "\n\n"
    elif ext == ".srt":
        # SubRip format
        blocks = raw.split("\n\n")
        for block in blocks:
            lines = block.strip().split("\n")
            # Skip number and timing lines
            text_lines = [l for l in lines[2:] if l.strip()] if len(lines) > 2 else []
            if text_lines:
                md += " ".join(text_lines) + "\n\n"

    return md


def _parse_generic_json(raw: str, filename: str) -> str:
    """Parse generic JSON into readable markdown."""
    try:
        data = json.loads(raw)
    except json.JSONDecodeError:
        return f"# JSON: {filename}\n\n[Invalid JSON]"

    md = f"# {filename}\n\n"

    if isinstance(data, list):
        md += f"**Array with {len(data)} items**\n\n"
        for i, item in enumerate(data[:20]):
            md += f"### Item {i + 1}\n"
            if isinstance(item, dict):
                for k, v in item.items():
                    md += f"- **{k}**: {str(v)[:200]}\n"
            else:
                md += f"{str(item)[:500]}\n"
            md += "\n"
    elif isinstance(data, dict):
        for k, v in data.items():
            if isinstance(v, (dict, list)):
                md += f"## {k}\n```json\n{json.dumps(v, indent=2)[:1000]}\n```\n\n"
            else:
                md += f"- **{k}**: {str(v)[:500]}\n"

    return md


def _parse_generic_jsonl(raw: str, filename: str) -> str:
    """Parse generic JSONL into readable markdown."""
    lines = raw.strip().split("\n")
    items = []
    for line in lines:
        try:
            items.append(json.loads(line))
        except json.JSONDecodeError:
            continue

    md = f"# {filename}\n\n**Records:** {len(items)}\n\n"

    for i, item in enumerate(items[:30]):
        if isinstance(item, dict):
            summary = ", ".join(f"{k}={str(v)[:50]}" for k, v in list(item.items())[:5])
            md += f"- Record {i + 1}: {summary}\n"
        else:
            md += f"- Record {i + 1}: {str(item)[:200]}\n"

    return md
