# tools/pdf_tools.py - PDF split, merge, extract text. Path resolution via utils.path_safety.
from __future__ import annotations

import json
import os

from langchain_core.tools import tool
from pypdf import PdfReader, PdfWriter

from tools._logging import _debug_log, _emit_input_status
from tools.live_view_visuals import emit_live_view_preview, image_to_preview_base64
from utils.path_safety import safe_resolve_path, is_dangerous_path, is_plugins_dir_write_blocked

_DEFAULT_WORKSPACE = os.path.expanduser("~/.mugi/workspace")


def _resolve_read(workspace: str, requested: str) -> tuple[str | None, str]:
    return safe_resolve_path(workspace or _DEFAULT_WORKSPACE, requested, allow_write=False, create_parents=False)


def _resolve_write(workspace: str, requested: str) -> tuple[str | None, str]:
    return safe_resolve_path(workspace or _DEFAULT_WORKSPACE, requested, allow_write=True)


def _looks_like_metadata_only(text: str) -> bool:
    """True if extracted 'text' is likely PDF/XMP metadata rather than document content.
    pypdf can return language codes, color profile names, gamma, etc. for image-based PDFs."""
    if not text or len(text.strip()) > 2000:
        return False
    t = text.lower()
    # Common metadata tokens that appear when there is no real page text
    metadata_indicators = (
        "gts_", "gamma", "color profile", "colorprofile",
        "enus", "frfr", "dede", "eses", "iten", "ptbr", "zhcn", "jajp",  # language codes
        "calrgb", "srgb", "devicegray", "icc", "xmp",
    )
    return sum(1 for m in metadata_indicators if m in t) >= 2


def extract_pdf_text_from_path(file_path: str) -> str:
    """Plain callable for PDF text extraction (knowledge digest, tests). Agent UI uses ``extract_pdf_text`` tool."""
    _debug_log(f"[DEBUG extract_pdf_text] Called with file_path: {file_path!r}")
    _emit_input_status("Extracting PDF text…")
    try:
        file_path = file_path.strip(' "\n,')
        abs_path, err = _resolve_read(_DEFAULT_WORKSPACE, file_path)
        if err:
            _debug_log(f"[ERROR extract_pdf_text] {err}", "ERROR")
            return err
        if not os.path.exists(abs_path):
            return f"File not found at: {abs_path}."
        if os.path.isdir(abs_path):
            return f"Path is a directory, not a file: {abs_path}\n\nUse 'list_directory' to list directory contents instead."
        if not os.path.isfile(abs_path):
            return f"Path exists but is not a file: {abs_path}."
        if not os.access(abs_path, os.R_OK):
            return f"File not readable: {abs_path}."
        with open(abs_path, "rb") as file:
            reader = PdfReader(file)
            num_pages = len(reader.pages)
            _debug_log(f"[DEBUG extract_pdf_text] PDF has {num_pages} page(s)")
            text = ""
            for page_num, page in enumerate(reader.pages, 1):
                page_text = page.extract_text()
                text += page_text + "\n"
        # Emit first-page preview if pdf2image is available
        try:
            from pdf2image import convert_from_path as _pdf_convert
            first_page_imgs = _pdf_convert(abs_path, dpi=150, first_page=1, last_page=1)
            if first_page_imgs:
                b64 = image_to_preview_base64(first_page_imgs[0], max_width=360)
                if b64:
                    emit_live_view_preview(
                        f"PDF: {os.path.basename(abs_path)} ({num_pages} pages)",
                        tool_name="extract_pdf_text",
                        preview_image_base64=b64,
                        preview_text=text[:300] if text.strip() else None,
                    )
        except Exception:
            pass
        if not text.strip():
            _debug_log("[WARN extract_pdf_text] No text extracted", "WARN")
            return (
                "No text extracted from PDF. This PDF is likely image-based (scanned). "
                "Use ocr_complex_document or ocr_with_kraken_api to extract text from the page images."
            )
        if _looks_like_metadata_only(text):
            _debug_log("[WARN extract_pdf_text] Extracted content looks like metadata only", "WARN")
            return (
                "The PDF appears to be image-based (scanned); only metadata was extracted (e.g. color profile, language codes), not page content. "
                "Use ocr_complex_document or ocr_with_kraken_api to extract the actual text from the page images."
            )
        _debug_log(f"[DEBUG extract_pdf_text] Total extracted: {len(text)} characters")
        return text
    except Exception as e:
        _debug_log(f"[ERROR extract_pdf_text] {e}", "ERROR")
        return f"PDF extraction failed: {str(e)}."


@tool
def extract_pdf_text(file_path: str) -> str:
    """Extract text from a PDF file. Use only when the PDF has selectable/embedded text.
    For scanned or image-based PDFs, use ocr_complex_document or ocr_with_kraken_api instead.
    Input: full file path to the PDF."""
    return extract_pdf_text_from_path(file_path)


@tool
def split_pdf(input_json: str) -> str:
    """Split a PDF file into multiple smaller PDF files.

    Input must be a JSON string with these keys:
    - "file_path": The full path to the source PDF file (required)
    - "output_dir": Directory where split PDFs will be saved (optional, defaults to same directory as source)
    - "split_method": How to split - "equal" (split into N equal parts) or "pages" (specify page ranges) (default: "equal")
    - "num_parts": Number of parts to split into (required if split_method is "equal")
    - "page_ranges": List of page ranges, e.g. [[1, 10], [11, 20], [21, 30]] (required if split_method is "pages")
         Page numbers are 1-indexed (first page is 1).

    Returns the paths of all created PDF files.
    Example: split_pdf('{"file_path": "/path/to/file.pdf", "split_method": "equal", "num_parts": 3}')
    """
    _debug_log(f"[DEBUG split_pdf] Called with input_json: {input_json!r}")
    _emit_input_status("Splitting PDF…")
    try:
        if isinstance(input_json, dict):
            data = input_json
        elif isinstance(input_json, str) and input_json.strip():
            data = json.loads(input_json)
        else:
            data = {}
        file_path = data.get("file_path", "").strip()
        output_dir = data.get("output_dir", "").strip()
        split_method = data.get("split_method", "equal").strip().lower()
        num_parts = data.get("num_parts")
        page_ranges = data.get("page_ranges", [])

        if not file_path:
            return "Error: 'file_path' is required."

        abs_path, err = _resolve_read(_DEFAULT_WORKSPACE, file_path)
        if err:
            return err
        if not os.path.exists(abs_path):
            return f"File not found: {abs_path}"
        if os.path.isdir(abs_path):
            return f"Path is a directory, not a file: {abs_path}\n\nUse 'list_directory' to list directory contents instead."
        if not os.path.isfile(abs_path):
            return f"Path exists but is not a file: {abs_path}"

        if output_dir:
            output_dir_abs, err = _resolve_write(_DEFAULT_WORKSPACE, output_dir)
            if err:
                return err
        else:
            output_dir_abs = os.path.dirname(abs_path)

        if is_plugins_dir_write_blocked(output_dir_abs):
            return "Blocked: cannot write to the plugins directory. Use create_plugin (evolution mode) to add plugins."
        from utils.user_model import enforce_low_level_user_model_write_guard

        _um_err = enforce_low_level_user_model_write_guard(output_dir_abs, mode="overwrite")
        if _um_err:
            return _um_err
        if not os.path.exists(output_dir_abs):
            return f"Output directory does not exist: {output_dir_abs}"
        if not os.access(output_dir_abs, os.W_OK):
            return f"Output directory is not writable: {output_dir_abs}"

        with open(abs_path, "rb") as file:
            reader = PdfReader(file)
            total_pages = len(reader.pages)
            _debug_log(f"[DEBUG split_pdf] Source PDF has {total_pages} page(s)")

        if split_method == "equal":
            if not num_parts or num_parts < 2:
                return "Error: 'num_parts' must be at least 2 when using 'equal' split method."
            pages_per_part = total_pages // num_parts
            remainder = total_pages % num_parts
            page_ranges = []
            start = 0
            for i in range(num_parts):
                end = start + pages_per_part + (1 if i < remainder else 0)
                page_ranges.append([start, end - 1])
                start = end
        elif split_method == "pages":
            if not page_ranges or not isinstance(page_ranges, list):
                return "Error: 'page_ranges' must be a list of [start, end] pairs when using 'pages' split method."
            page_ranges = [[start - 1, end - 1] for start, end in page_ranges]
        else:
            return f"Error: 'split_method' must be 'equal' or 'pages', got '{split_method}'"

        num_total_parts = len(page_ranges)
        emit_live_view_preview(
            f"Splitting {os.path.basename(abs_path)} into {num_total_parts} parts…",
            tool_name="split_pdf",
            progress_pct=0,
        )
        try:
            from pdf2image import convert_from_path as _pdf_convert
            _pdf2img_ok = True
        except ImportError:
            _pdf2img_ok = False

        source_name = os.path.splitext(os.path.basename(abs_path))[0]
        output_files = []
        with open(abs_path, "rb") as file:
            reader = PdfReader(file)
            for idx, (start_page, end_page) in enumerate(page_ranges, 1):
                if start_page < 0 or end_page >= total_pages or start_page > end_page:
                    continue
                writer = PdfWriter()
                for page_num in range(start_page, end_page + 1):
                    writer.add_page(reader.pages[page_num])
                output_filename = f"{source_name}_part{idx}.pdf"
                output_path = os.path.join(output_dir_abs, output_filename)
                with open(output_path, "wb") as output_file:
                    writer.write(output_file)
                output_files.append(output_path)
                pct = int(100 * idx / num_total_parts)
                part_pages = end_page - start_page + 1
                b64 = None
                if _pdf2img_ok:
                    try:
                        imgs = _pdf_convert(output_path, dpi=150, first_page=1, last_page=1)
                        if imgs:
                            b64 = image_to_preview_base64(imgs[0], max_width=400)
                    except Exception:
                        pass
                emit_live_view_preview(
                    f"Part {idx}/{num_total_parts} — pages {start_page + 1}–{end_page + 1} ({part_pages} pg)",
                    tool_name="split_pdf",
                    progress_pct=pct,
                    preview_image_base64=b64,
                )

        if not output_files:
            return "Error: No output files were created. Check page ranges."

        result = f"PDF split successfully into {len(output_files)} file(s):\n"
        for i, path in enumerate(output_files, 1):
            result += f"{i}. {path} ({os.path.getsize(path)} bytes)\n"
        result += (
            f"\n⚠️ Process ALL {len(output_files)} parts—do not stop after the first.\n"
            f"For OCR to one file: use ocr_scanned_pdf_to_file on each part (part1 append=False, part2+ append=True). "
            f"For other uses (email, print, merge later, etc.) use the part paths above as needed."
        )
        _debug_log("[PIPELINE] PDF SPLIT COMPLETE", "PIPELINE")
        return result
    except json.JSONDecodeError as e:
        return f"Invalid JSON input: {str(e)}"
    except Exception as e:
        _debug_log(f"[ERROR split_pdf] {e}", "ERROR")
        import traceback
        _debug_log(traceback.format_exc(), "ERROR")
        return f"PDF split failed: {str(e)}"


@tool
def merge_pdfs(input_json: str) -> str:
    """Merge multiple PDF files into a single PDF file.

    Input must be a JSON string with these keys:
    - "file_paths": List of full paths to PDF files to merge (required, at least 2 files)
    - "output_path": Full path for the merged PDF file (required)

    PDFs will be merged in the order they appear in the file_paths list.
    Example: merge_pdfs('{"file_paths": ["/path/to/file1.pdf", "/path/to/file2.pdf"], "output_path": "/path/to/merged.pdf"}')
    """
    _debug_log(f"[DEBUG merge_pdfs] Called with input_json: {input_json!r}")
    _emit_input_status("Merging PDFs…")
    try:
        if isinstance(input_json, dict):
            data = input_json
        elif isinstance(input_json, str) and input_json.strip():
            data = json.loads(input_json)
        else:
            data = {}
        file_paths = data.get("file_paths", [])
        output_path = data.get("output_path", "").strip()

        if not file_paths or not isinstance(file_paths, list):
            return "Error: 'file_paths' must be a list with at least 2 PDF file paths."
        if len(file_paths) < 2:
            return "Error: At least 2 PDF files are required to merge."
        if not output_path:
            return "Error: 'output_path' is required."

        out_abs, err = _resolve_write(_DEFAULT_WORKSPACE, output_path)
        if err:
            return err
        if is_plugins_dir_write_blocked(out_abs):
            return "Blocked: cannot write to the plugins directory. Use create_plugin (evolution mode) to add plugins."
        from utils.user_model import enforce_low_level_user_model_write_guard

        _um_err = enforce_low_level_user_model_write_guard(out_abs, mode="overwrite")
        if _um_err:
            return _um_err
        output_dir = os.path.dirname(out_abs)
        if not os.path.exists(output_dir):
            os.makedirs(output_dir, exist_ok=True)
        if not os.access(output_dir, os.W_OK):
            return f"Output directory is not writable: {output_dir}"

        n_files = len(file_paths)
        emit_live_view_preview(
            f"Merging {n_files} PDFs…",
            tool_name="merge_pdfs",
            progress_pct=0,
        )
        writer = PdfWriter()
        for idx, fp in enumerate(file_paths):
            fp = (fp or "").strip()
            abs_fp, err = _resolve_read(_DEFAULT_WORKSPACE, fp)
            if err:
                return err
            if not os.path.exists(abs_fp):
                return f"File not found: {abs_fp}"
            if os.path.isdir(abs_fp):
                return f"Path is a directory, not a file: {abs_fp}"
            with open(abs_fp, "rb") as f:
                reader = PdfReader(f)
                for page in reader.pages:
                    writer.add_page(page)
            pct = int(100 * (idx + 1) / n_files) if n_files else 100
            emit_live_view_preview(
                f"Adding file {idx + 1}/{n_files}…",
                tool_name="merge_pdfs",
                progress_pct=pct,
            )

        with open(out_abs, "wb") as f:
            writer.write(f)
        size = os.path.getsize(out_abs)
        b64 = None
        try:
            from pdf2image import convert_from_path as _pdf_convert
            imgs = _pdf_convert(out_abs, dpi=150, first_page=1, last_page=1)
            if imgs:
                b64 = image_to_preview_base64(imgs[0], max_width=400)
        except Exception:
            pass
        emit_live_view_preview(
            f"Merged: {os.path.basename(out_abs)} ({size} bytes)",
            tool_name="merge_pdfs",
            progress_pct=100,
            preview_image_base64=b64,
        )
        result = (
            f"PDFs merged successfully!\n"
            f"Output file: {out_abs}\n"
            f"File size: {size} bytes\n"
            f"Merged {len(file_paths)} file(s) in order."
        )
        _debug_log(f"[DEBUG merge_pdfs] ========== FUNCTION RETURNING (success) ==========")
        return result
    except json.JSONDecodeError as e:
        return f"Invalid JSON input: {str(e)}"
    except Exception as e:
        _debug_log(f"[ERROR merge_pdfs] {e}", "ERROR")
        import traceback
        _debug_log(traceback.format_exc(), "ERROR")
        return f"PDF merge failed: {str(e)}"


__all__ = ["extract_pdf_text", "extract_pdf_text_from_path", "split_pdf", "merge_pdfs"]
