"""extract_doc_text -- Extract readable text from downloaded documents.

Scans the release-docs/ folder for downloaded PDFs and other binary
documents, extracts their text content, and saves as .txt files
alongside the originals. Returns metadata only (not full text) so the
AI can call this in parallel across packages, then read the .txt files
sequentially for analysis.

Extraction is idempotent -- files with an existing .txt companion are
skipped. Re-running after new downloads picks up only the new files.
"""

import logging
import re
import xml.etree.ElementTree as ET
import zipfile
from io import BytesIO
from pathlib import Path
from typing import Any

from ..utils import state

logger = logging.getLogger(__name__)

_SKIP_EXTENSIONS = {".md", ".txt", ".html", ".htm", ".json", ".csv"}
_BINARY_DOC_EXTENSIONS = {".pdf", ".docx", ".pptx"}

try:
    import fitz  # PyMuPDF

    _HAS_PYMUPDF = True
except ImportError:
    _HAS_PYMUPDF = False

TOOL_DEFINITION = {
    "name": "extract_doc_text",
    "description": (
        "Extract readable text from downloaded PDFs and documents. "
        "Scans the release-docs/ folder for binary files (PDF, DOCX), "
        "extracts text content, and saves as .txt files alongside "
        "the originals. Returns per-package metadata (file counts, "
        "word counts, paths) — not the full text. The AI should call "
        "this in parallel across packages, then read the .txt files "
        "sequentially for analysis. Idempotent — existing .txt files "
        "are skipped."
    ),
    "inputSchema": {
        "type": "object",
        "properties": {
            "working_directory": {
                "type": "string",
                "description": "Absolute path to the workspace root directory.",
            },
            "account_name": {
                "type": "string",
                "description": (
                    "Optional customer account name. Determines the base "
                    "directory: customers/<slug>/release-docs/ if set, "
                    "otherwise release-docs/."
                ),
            },
            "packages": {
                "type": "array",
                "items": {"type": "string"},
                "description": (
                    "Optional list of package folder names to process. "
                    "Use the folder names from release-docs/ (e.g. "
                    "'CloudSense_CPQ_(Configurator)'). If omitted, "
                    "processes all packages in the release-docs folder."
                ),
            },
        },
        "required": ["working_directory"],
    },
}


def _make_slug(name: str) -> str:
    slug = name.lower().strip()
    slug = re.sub(r"[^a-z0-9]+", "-", slug)
    slug = slug.strip("-")
    return slug or "unknown"


def _txt_path_for(file_path: Path) -> Path:
    """Return the companion .txt path for a given file."""
    return file_path.parent / f"{file_path.name}.txt"


def _extract_pdf(file_path: Path) -> str:
    """Extract text from a PDF using PyMuPDF."""
    doc = fitz.open(str(file_path))
    pages = []
    for page in doc:
        text = page.get_text()
        if text.strip():
            pages.append(text)
    doc.close()
    return "\n\n---\n\n".join(pages)


def _extract_docx(file_path: Path) -> str:
    """Extract text from a DOCX by parsing the inner XML."""
    if _HAS_PYMUPDF:
        try:
            doc = fitz.open(str(file_path))
            pages = []
            for page in doc:
                text = page.get_text()
                if text.strip():
                    pages.append(text)
            doc.close()
            if pages:
                return "\n\n---\n\n".join(pages)
        except Exception:
            pass

    texts = []
    try:
        with zipfile.ZipFile(file_path) as zf:
            if "word/document.xml" in zf.namelist():
                xml_content = zf.read("word/document.xml")
                tree = ET.parse(BytesIO(xml_content))
                ns = {
                    "w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
                }
                for para in tree.iter(f"{{{ns['w']}}}p"):
                    para_texts = []
                    for run in para.iter(f"{{{ns['w']}}}t"):
                        if run.text:
                            para_texts.append(run.text)
                    if para_texts:
                        texts.append("".join(para_texts))
    except Exception as e:
        logger.warning("DOCX XML extraction failed for %s: %s", file_path, e)
    return "\n".join(texts)


def _extract_text(file_path: Path) -> tuple[str | None, str | None]:
    """Extract text from a file. Returns (text, error)."""
    ext = file_path.suffix.lower()

    if ext == ".pdf":
        if not _HAS_PYMUPDF:
            return None, "PyMuPDF not installed"
        try:
            text = _extract_pdf(file_path)
            return text, None
        except Exception as e:
            return None, f"PDF extraction failed: {e}"

    elif ext == ".docx":
        try:
            text = _extract_docx(file_path)
            return text, None
        except Exception as e:
            return None, f"DOCX extraction failed: {e}"

    elif ext == ".pptx":
        if _HAS_PYMUPDF:
            try:
                doc = fitz.open(str(file_path))
                pages = [p.get_text() for p in doc if p.get_text().strip()]
                doc.close()
                if pages:
                    return "\n\n---\n\n".join(pages), None
            except Exception:
                pass
        return None, "PPTX extraction not supported without PyMuPDF"

    elif ext in {".xlsx", ".xls"}:
        return None, "Spreadsheet — skipped (not release notes content)"

    else:
        return None, f"Unknown file type: {ext}"


def _scan_and_extract(
    base_dir: Path, package_filter: list[str] | None,
) -> tuple[list[dict[str, Any]], list[dict[str, Any]], list[dict[str, Any]]]:
    """Scan release-docs and extract text from binary files.

    Returns (extracted, skipped, failed) lists.
    """
    extracted: list[dict[str, Any]] = []
    skipped: list[dict[str, Any]] = []
    failed: list[dict[str, Any]] = []

    filter_lower = {p.lower() for p in package_filter} if package_filter else None

    for pkg_dir in sorted(base_dir.iterdir()):
        if not pkg_dir.is_dir() or pkg_dir.name == "__pycache__":
            continue
        if filter_lower and pkg_dir.name.lower() not in filter_lower:
            continue

        for release_dir in sorted(pkg_dir.iterdir()):
            if not release_dir.is_dir():
                continue

            for file_path in sorted(release_dir.iterdir()):
                if not file_path.is_file():
                    continue
                if file_path.suffix.lower() in _SKIP_EXTENSIONS:
                    continue

                txt_out = _txt_path_for(file_path)
                entry = {
                    "package": pkg_dir.name,
                    "release": release_dir.name,
                    "file": file_path.name,
                    "txt_path": str(txt_out),
                }

                if txt_out.exists() and txt_out.stat().st_size > 0:
                    word_count = len(txt_out.read_text(errors="replace").split())
                    skipped.append({**entry, "words": word_count})
                    continue

                text, error = _extract_text(file_path)

                if error:
                    failed.append({**entry, "error": error})
                    continue

                if not text or not text.strip():
                    failed.append({
                        **entry,
                        "error": "Extracted text is empty (possibly scanned/image PDF)",
                    })
                    continue

                txt_out.write_text(text, encoding="utf-8")
                word_count = len(text.split())
                extracted.append({**entry, "words": word_count})

    return extracted, skipped, failed


async def run(arguments: dict[str, Any]) -> str:
    """Execute the extract_doc_text tool."""
    state.set_working_dir(arguments.get("working_directory"))
    working_dir = state.get_working_dir()

    account_name = arguments.get("account_name", "").strip() or None
    package_filter = arguments.get("packages") or None

    if account_name:
        base_dir = working_dir / "customers" / _make_slug(account_name) / "release-docs"
    else:
        base_dir = working_dir / "release-docs"

    if not base_dir.exists():
        return (
            f"STATUS: NO_RELEASE_DOCS\n\n"
            f"No release-docs folder found at {base_dir}.\n\n"
            f"Call get_release_info first to create the folder structure "
            f"with document links, then download_release_docs to download "
            f"the actual files, then call this tool to extract text."
        )

    if not _HAS_PYMUPDF:
        binary_files = []
        for pkg_dir in base_dir.iterdir():
            if not pkg_dir.is_dir():
                continue
            for release_dir in pkg_dir.iterdir():
                if not release_dir.is_dir():
                    continue
                for f in release_dir.iterdir():
                    if f.is_file() and f.suffix.lower() in _BINARY_DOC_EXTENSIONS:
                        binary_files.append(f)

        if binary_files:
            return (
                "STATUS: PYMUPDF_MISSING\n\n"
                f"Found {len(binary_files)} PDF/DOCX files but PyMuPDF is "
                "not installed. Cannot extract text from binary documents.\n\n"
                "Install it with: pip install pymupdf\n\n"
                "Then restart the MCP server and try again."
            )

    extracted, skipped, failed = _scan_and_extract(base_dir, package_filter)

    total = len(extracted) + len(skipped) + len(failed)

    if total == 0:
        hint = ""
        if package_filter:
            available = sorted(
                d.name for d in base_dir.iterdir()
                if d.is_dir() and d.name != "__pycache__"
            )
            hint = (
                f"\n\nAvailable packages: {', '.join(available)}"
                if available else ""
            )
        return (
            "STATUS: NO_FILES\n\n"
            "No downloadable document files found in the release-docs "
            "folder. Only .md link files are present.\n\n"
            "Call download_release_docs first to download the actual "
            "documents from Google Drive, then call this tool to extract "
            "text." + hint
        )

    pkg_stats: dict[str, dict[str, Any]] = {}
    for e in extracted:
        pkg = e["package"]
        pkg_stats.setdefault(pkg, {
            "extracted": 0, "skipped": 0, "failed": 0,
            "total_words": 0, "txt_paths": [],
        })
        pkg_stats[pkg]["extracted"] += 1
        pkg_stats[pkg]["total_words"] += e.get("words", 0)
        pkg_stats[pkg]["txt_paths"].append(e["txt_path"])

    for s in skipped:
        pkg = s["package"]
        pkg_stats.setdefault(pkg, {
            "extracted": 0, "skipped": 0, "failed": 0,
            "total_words": 0, "txt_paths": [],
        })
        pkg_stats[pkg]["skipped"] += 1
        pkg_stats[pkg]["total_words"] += s.get("words", 0)
        pkg_stats[pkg]["txt_paths"].append(s["txt_path"])

    for f in failed:
        pkg = f["package"]
        pkg_stats.setdefault(pkg, {
            "extracted": 0, "skipped": 0, "failed": 0,
            "total_words": 0, "txt_paths": [],
        })
        pkg_stats[pkg]["failed"] += 1

    total_words = sum(s["total_words"] for s in pkg_stats.values())

    lines = [
        "STATUS: READY\n",
        "## Extraction Results\n",
        f"Extracted: {len(extracted)} files",
        f"Skipped (already extracted): {len(skipped)}",
        f"Failed: {len(failed)}",
        f"Total files: {total}",
        f"Total words: {total_words:,}",
        "",
        "### Per-package:\n",
    ]

    for pkg in sorted(pkg_stats.keys()):
        stats = pkg_stats[pkg]
        parts = []
        if stats["extracted"]:
            parts.append(f"{stats['extracted']} extracted")
        if stats["skipped"]:
            parts.append(f"{stats['skipped']} already done")
        if stats["failed"]:
            parts.append(f"{stats['failed']} FAILED")
        words_k = stats["total_words"] / 1000
        parts.append(f"~{words_k:.0f}k words")
        lines.append(f"  {pkg}: {', '.join(parts)}")

    if failed:
        lines.append("\n### Failed extractions:\n")
        for f in failed:
            lines.append(
                f"  - {f['package']}/{f['release']}: "
                f"{f['file']} — {f.get('error', 'unknown')}"
            )

    lines.extend([
        "",
        "### Text file locations:\n",
    ])

    for pkg in sorted(pkg_stats.keys()):
        txt_files = pkg_stats[pkg]["txt_paths"]
        if txt_files:
            lines.append(f"  {pkg}: {len(txt_files)} .txt files in release-docs/{pkg}/*/")

    lines.extend([
        "",
        "---",
        "",
        "The AI should now read the extracted .txt files per-package ",
        "to analyse release notes content. Read one package at a time ",
        "to manage context size.",
    ])

    return "\n".join(lines)
