"""PDF and document reading tool."""

from __future__ import annotations

import asyncio
import json
import shutil
from pathlib import Path
from typing import Any

from loguru import logger

from madpod_agent.agent.tools.base import Tool

_MAX_TEXT_CHARS = 120_000  # ~120 KB context budget


def _resolve_safe(path_str: str, workspace: Path, allowed_dir: Path | None) -> Path:
    """Resolve path against workspace with optional jail."""
    p = Path(path_str).expanduser()
    if not p.is_absolute():
        p = workspace / p
    resolved = p.resolve()
    if allowed_dir:
        try:
            resolved.relative_to(allowed_dir.resolve())
        except ValueError:
            raise PermissionError(f"Path {path_str} is outside allowed directory")
    return resolved


class PDFReadTool(Tool):
    """Read text content from PDF files."""

    def __init__(self, workspace: Path, allowed_dir: Path | None = None):
        self._workspace = workspace
        self._allowed_dir = allowed_dir

    @property
    def name(self) -> str:
        return "pdf_read"

    @property
    def description(self) -> str:
        return (
            "Extract text content from a PDF file. Returns the text of specified pages or the full document. "
            "Use this for reading downloaded PDFs, invoices, contracts, reports, or any PDF document. "
            "Supports page ranges to avoid loading huge documents entirely."
        )

    @property
    def parameters(self) -> dict[str, Any]:
        return {
            "type": "object",
            "properties": {
                "path": {"type": "string", "description": "Path to the PDF file"},
                "pages": {
                    "type": "string",
                    "description": "Page range like '1-5', '3', '1,3,5-8'. Omit for all pages.",
                },
                "max_chars": {
                    "type": "integer",
                    "description": "Max characters to return (default 120000)",
                },
            },
            "required": ["path"],
        }

    async def execute(self, path: str, pages: str | None = None, max_chars: int | None = None, **kwargs: Any) -> str:
        limit = min(max_chars or _MAX_TEXT_CHARS, _MAX_TEXT_CHARS)

        try:
            file_path = _resolve_safe(path, self._workspace, self._allowed_dir)
        except PermissionError as exc:
            return json.dumps({"error": str(exc)})

        if not file_path.exists():
            return json.dumps({"error": f"File not found: {path}"})
        if not file_path.suffix.lower() == ".pdf":
            return json.dumps({"error": f"Not a PDF file: {file_path.name}"})

        page_set = _parse_page_range(pages) if pages else None

        # Try pdfplumber first (best quality), fall back to pdftotext CLI
        text, method, total_pages, error = await _extract_with_pdfplumber(file_path, page_set)
        if error:
            text, method, total_pages, error = await _extract_with_pdftotext(file_path, page_set)
        if error:
            text, method, total_pages, error = await _extract_with_pymupdf(file_path, page_set)
        if error:
            return json.dumps({"error": f"Failed to read PDF: {error}. Install pdfplumber, pymupdf, or pdftotext."})

        truncated = len(text) > limit
        if truncated:
            text = text[:limit] + "\n\n... (truncated)"

        return json.dumps({
            "ok": True,
            "path": str(file_path),
            "total_pages": total_pages,
            "pages_read": pages or "all",
            "chars": len(text),
            "truncated": truncated,
            "method": method,
            "text": text,
        })


def _parse_page_range(spec: str) -> set[int]:
    """Parse '1-5,8,10-12' into a set of 1-based page numbers."""
    pages: set[int] = set()
    for part in spec.split(","):
        part = part.strip()
        if "-" in part:
            start, end = part.split("-", 1)
            pages.update(range(int(start), int(end) + 1))
        elif part.isdigit():
            pages.add(int(part))
    return pages


async def _extract_with_pdfplumber(
    path: Path, page_set: set[int] | None
) -> tuple[str, str, int, str | None]:
    """Extract using pdfplumber (high quality, handles tables well)."""
    try:
        import pdfplumber
    except ImportError:
        return "", "", 0, "pdfplumber not installed"

    def _read() -> tuple[str, int]:
        parts: list[str] = []
        with pdfplumber.open(path) as pdf:
            total = len(pdf.pages)
            for i, page in enumerate(pdf.pages):
                page_num = i + 1
                if page_set and page_num not in page_set:
                    continue
                text = page.extract_text() or ""
                if text.strip():
                    parts.append(f"--- Page {page_num} ---\n{text}")
            return "\n\n".join(parts), total

    try:
        text, total = await asyncio.to_thread(_read)
        return text, "pdfplumber", total, None
    except Exception as exc:
        return "", "", 0, str(exc)


async def _extract_with_pymupdf(
    path: Path, page_set: set[int] | None
) -> tuple[str, str, int, str | None]:
    """Extract using PyMuPDF (fitz)."""
    try:
        import fitz  # noqa: F401 — PyMuPDF
    except ImportError:
        return "", "", 0, "pymupdf not installed"

    def _read() -> tuple[str, int]:
        parts: list[str] = []
        doc = fitz.open(str(path))
        total = len(doc)
        for i in range(total):
            page_num = i + 1
            if page_set and page_num not in page_set:
                continue
            text = doc[i].get_text() or ""
            if text.strip():
                parts.append(f"--- Page {page_num} ---\n{text}")
        doc.close()
        return "\n\n".join(parts), total

    try:
        text, total = await asyncio.to_thread(_read)
        return text, "pymupdf", total, None
    except Exception as exc:
        return "", "", 0, str(exc)


async def _extract_with_pdftotext(
    path: Path, page_set: set[int] | None
) -> tuple[str, str, int, str | None]:
    """Fall back to pdftotext CLI."""
    if not shutil.which("pdftotext"):
        return "", "", 0, "pdftotext not installed"

    try:
        args = ["pdftotext", "-layout"]
        if page_set:
            sorted_pages = sorted(page_set)
            args.extend(["-f", str(sorted_pages[0]), "-l", str(sorted_pages[-1])])
        args.extend([str(path), "-"])

        proc = await asyncio.create_subprocess_exec(
            *args,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
        )
        stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=30)

        if proc.returncode != 0:
            return "", "", 0, f"pdftotext exit {proc.returncode}: {stderr.decode(errors='replace')}"

        text = stdout.decode("utf-8", errors="replace")
        # Rough page count from form feeds
        total_pages = text.count("\f") + 1
        return text, "pdftotext", total_pages, None
    except Exception as exc:
        return "", "", 0, str(exc)
