"""File extraction: turn a path into text units with locators.

Text extraction here feeds every text-based detector. Metadata extraction (R-4)
is layered on in metadata.py and called from the engine; this module fills
ExtractedDoc.units and full_text and flags image-only PDFs for manual review
(Non-Goal 2: no OCR, never a silent pass).
"""
from __future__ import annotations

import os
from typing import Optional

from .models import ExtractedDoc, TextUnit

TEXT_EXTS = {".txt", ".md", ".text", ".log"}
EML_EXTS = {".eml", ".msg"}
DOCX_EXTS = {".docx"}
PDF_EXTS = {".pdf"}
IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".tif", ".tiff", ".webp", ".heic", ".gif"}

SUPPORTED_EXTS = TEXT_EXTS | EML_EXTS | DOCX_EXTS | PDF_EXTS | IMAGE_EXTS

# Additional text-like extensions scanned only under the repo profile, where source
# and config files are the usual home of secrets.
REPO_TEXT_EXTS = {
    ".env", ".pem", ".key", ".py", ".js", ".ts", ".tsx", ".jsx", ".yml", ".yaml",
    ".json", ".cfg", ".ini", ".conf", ".sh", ".bash", ".tf", ".tfvars", ".toml",
    ".properties", ".xml", ".gradle", ".rb", ".go", ".rs", ".java", ".php", ".pl",
    ".ps1", ".bat",
}


def kind_for(path: str) -> Optional[str]:
    ext = os.path.splitext(path)[1].lower()
    if ext in DOCX_EXTS:
        return "docx"
    if ext in PDF_EXTS:
        return "pdf"
    if ext in IMAGE_EXTS:
        return "image"
    if ext in EML_EXTS:
        return "eml"
    if ext in TEXT_EXTS:
        return "txt"
    return None


def _build(units_raw: list[tuple[str, str]], path: str, kind: str,
           notes: Optional[list[str]] = None) -> ExtractedDoc:
    """Assemble an ExtractedDoc from (text, locator) pairs, tracking offsets."""
    units: list[TextUnit] = []
    parts: list[str] = []
    offset = 0
    for text, locator in units_raw:
        units.append(TextUnit(text=text, locator=locator, base_offset=offset))
        parts.append(text)
        offset += len(text) + 1  # +1 for the "\n" join below
    full_text = "\n".join(parts)
    return ExtractedDoc(path=path, kind=kind, units=units, full_text=full_text,
                        notes=list(notes or []))


def _extract_txt(path: str, kind: str) -> ExtractedDoc:
    with open(path, "r", encoding="utf-8", errors="replace") as fh:
        content = fh.read()
    units_raw = [(line, f"line {i}") for i, line in enumerate(content.splitlines(), 1)]
    if not units_raw:
        units_raw = [("", "line 1")]
    return _build(units_raw, path, kind)


def _extract_docx(path: str) -> ExtractedDoc:
    import docx  # python-docx

    document = docx.Document(path)
    units_raw: list[tuple[str, str]] = []
    for i, para in enumerate(document.paragraphs, 1):
        units_raw.append((para.text, f"paragraph {i}"))
    # Include table cell text so PII in tables is not missed.
    for ti, table in enumerate(document.tables, 1):
        for ri, row in enumerate(table.rows, 1):
            for ci, cell in enumerate(row.cells, 1):
                if cell.text:
                    units_raw.append((cell.text, f"table {ti} row {ri} col {ci}"))
    if not units_raw:
        units_raw = [("", "paragraph 1")]
    return _build(units_raw, path, "docx")


def _extract_pdf(path: str) -> ExtractedDoc:
    from pypdf import PdfReader

    reader = PdfReader(path)
    units_raw: list[tuple[str, str]] = []
    notes: list[str] = []
    empty_pages = 0
    for i, page in enumerate(reader.pages, 1):
        try:
            text = page.extract_text() or ""
        except Exception:  # noqa: BLE001 - a malformed page should not abort the file
            text = ""
        if text.strip():
            units_raw.append((text, f"page {i}"))
        else:
            empty_pages += 1
    if empty_pages:
        notes.append(
            f"{os.path.basename(path)}: {empty_pages} page(s) have no extractable text "
            f"(image-only or scanned); review manually. No OCR is performed."
        )
    if not units_raw:
        units_raw = [("", "page 1")]
    return _build(units_raw, path, "pdf", notes=notes)


def _extract_image(path: str) -> ExtractedDoc:
    # No OCR (Non-Goal 2). Images carry no extractable text layer here; metadata
    # (EXIF) is scanned separately in metadata.py.
    return _build([("", "image")], path, "image")


def extract(path: str) -> ExtractedDoc:
    kind = kind_for(path)
    if kind == "docx":
        return _extract_docx(path)
    if kind == "pdf":
        return _extract_pdf(path)
    if kind == "image":
        return _extract_image(path)
    # txt and eml both read as text here; eml header/attachment handling is in the
    # email profile path.
    return _extract_txt(path, kind or "txt")
