"""Document conversion engine for StyloBridge.

Wraps Pandoc (Markdown → DOCX) and pdf2docx (PDF → DOCX) with
enterprise security features: workspace sandboxing, symlink
prevention, and corporate template support.
"""

import os
from pathlib import Path

import pypandoc
from docx import Document
from docx.oxml import parse_xml
from docx.oxml.ns import nsdecls
from docx.shared import Pt, RGBColor
from pdf2docx import Converter as PdfConverter

from src.preprocessor import preprocess_markdown, preprocess_markdown_content

HEADER_BLUE_HEX = "2E5090"
ALT_ROW_GRAY_HEX = "F2F2F2"
BORDER_COLOR_HEX = "BFBFBF"

# Auto-install Pandoc if not found — zero-dependency experience for customers
try:
    pypandoc.get_pandoc_version()
except OSError:
    pypandoc.ensure_pandoc_installed()

MAX_INPUT_SIZE = 50 * 1024 * 1024  # 50 MB


class DocumentEngine:
    """Core conversion engine with path safety and template support."""

    def __init__(self, template_dir: str | None = None):
        self.template_dir = Path(template_dir) if template_dir else None

    def _validate_path(self, path: str) -> Path:
        """Validate that a path is safe to access.

        Resolves the path against WORKSPACE_ROOT if not absolute.
        Rejects symlinks and paths outside WORKSPACE_ROOT.
        Re-checks symlink status on the resolved path to close
        TOCTOU gaps.
        """
        workspace_root = Path(os.getenv("WORKSPACE_ROOT", "./data")).resolve()
        p = Path(path)

        # If the path isn't absolute, resolve it relative to workspace root
        # so users can say "convert test.md" without full paths
        if not p.is_absolute():
            p = workspace_root / p

        if p.is_symlink():
            raise PermissionError("Access denied: symlinks are not allowed.")
        resolved = p.resolve()
        if not resolved.is_relative_to(workspace_root):
            raise PermissionError(
                f"Access denied: file must be inside the workspace directory "
                f"({workspace_root}). Move the file there or update WORKSPACE_ROOT."
            )
        # Second symlink check on resolved path to narrow TOCTOU window
        if resolved.is_symlink():
            raise PermissionError("Access denied: symlinks are not allowed.")
        if resolved.exists() and resolved.stat().st_size > MAX_INPUT_SIZE:
            raise ValueError(
                f"Input file exceeds maximum size of {MAX_INPUT_SIZE // (1024 * 1024)} MB."
            )
        return resolved

    def _validate_template_path(self, template_name: str) -> Path | None:
        """Validate that a template name resolves safely within template_dir.

        Rejects path traversal sequences, symlinks, and paths outside template_dir.
        """
        if not template_name or not self.template_dir:
            return None
        raw_path = self.template_dir / template_name
        # Check symlink before resolving (resolve follows symlinks)
        if raw_path.is_symlink():
            raise PermissionError("Access denied: symlinks are not allowed in templates.")
        template_path = raw_path.resolve()
        if not template_path.is_relative_to(self.template_dir.resolve()):
            raise PermissionError("Access denied: invalid template name.")
        if not template_path.exists():
            return None
        return template_path

    @staticmethod
    def _style_tables(docx_path: str) -> None:
        """Post-process DOCX to add styled table headers and alternating rows."""
        doc = Document(docx_path)
        for table in doc.tables:
            # Style header row (first row): blue background, white bold text
            if table.rows:
                for cell in table.rows[0].cells:
                    shading = parse_xml(
                        f'<w:shd {nsdecls("w")} '
                        f'w:fill="{HEADER_BLUE_HEX}" w:val="clear"/>'
                    )
                    cell._tc.get_or_add_tcPr().append(shading)
                    for p in cell.paragraphs:
                        for run in p.runs:
                            run.font.color.rgb = RGBColor(0xFF, 0xFF, 0xFF)
                            run.font.bold = True

            # Alternating row shading for data rows
            for i, row in enumerate(table.rows[1:], start=1):
                if i % 2 == 1:
                    for cell in row.cells:
                        shading = parse_xml(
                            f'<w:shd {nsdecls("w")} '
                            f'w:fill="{ALT_ROW_GRAY_HEX}" w:val="clear"/>'
                        )
                        cell._tc.get_or_add_tcPr().append(shading)
        doc.save(docx_path)

    def md_to_docx(
        self,
        input_path: str,
        output_path: str,
        template_name: str | None = None,
        include_toc: bool = True,
    ) -> str:
        """Convert Markdown to DOCX using Pandoc.

        Args:
            input_path: Path to the source .md file.
            output_path: Path for the output .docx file.
            template_name: Optional .docx template filename in template_dir.
            include_toc: Whether to generate a Table of Contents.

        Returns:
            Absolute path to the generated DOCX file.
        """
        validated_input = self._validate_path(input_path)
        validated_output = self._validate_path(output_path)

        if not validated_input.exists():
            raise FileNotFoundError(f"Input file not found: {input_path}")

        # Run Style Guard preprocessor
        preprocessed_path = preprocess_markdown(str(validated_input))

        try:
            extra_args = []

            # Apply corporate template, or fall back to built-in default
            validated_template = self._validate_template_path(template_name)
            if not validated_template and self.template_dir:
                default = self.template_dir / "default.docx"
                if default.exists():
                    validated_template = default
            if validated_template:
                extra_args += ["--reference-doc", str(validated_template)]

            # Add Table of Contents
            if include_toc:
                extra_args += ["--toc", "--toc-depth=3"]

            pypandoc.convert_file(
                preprocessed_path,
                "docx",
                format="markdown+pipe_tables+backtick_code_blocks+autolink_bare_uris",
                outputfile=str(validated_output),
                extra_args=extra_args,
            )
        finally:
            # Clean up preprocessed temp file
            Path(preprocessed_path).unlink(missing_ok=True)

        # Post-process: style tables with blue headers and alternating rows
        self._style_tables(str(validated_output))

        return str(validated_output)

    def md_content_to_docx(
        self,
        content: str,
        output_path: str,
        template_name: str | None = None,
        include_toc: bool = True,
    ) -> str:
        """Convert a markdown string to DOCX using Pandoc.

        Args:
            content: Raw markdown text.
            output_path: Path for the output .docx file (must be in workspace).
            template_name: Optional .docx template filename in template_dir.
            include_toc: Whether to generate a Table of Contents.

        Returns:
            Absolute path to the generated DOCX file.
        """
        validated_output = self._validate_path(output_path)

        # Preprocess the markdown content (string in, string out)
        cleaned = preprocess_markdown_content(content)

        extra_args = []

        # Apply corporate template, or fall back to built-in default
        validated_template = self._validate_template_path(template_name)
        if not validated_template and self.template_dir:
            default = self.template_dir / "default.docx"
            if default.exists():
                validated_template = default
        if validated_template:
            extra_args += ["--reference-doc", str(validated_template)]

        # Add Table of Contents
        if include_toc:
            extra_args += ["--toc", "--toc-depth=3"]

        pypandoc.convert_text(
            cleaned,
            "docx",
            format="markdown+pipe_tables+backtick_code_blocks+autolink_bare_uris",
            outputfile=str(validated_output),
            extra_args=extra_args,
        )

        # Post-process: style tables with blue headers and alternating rows
        self._style_tables(str(validated_output))

        return str(validated_output)

    def pdf_to_docx(self, input_path: str, output_path: str) -> str:
        """Convert PDF to DOCX using pdf2docx.

        Args:
            input_path: Path to the source .pdf file.
            output_path: Path for the output .docx file.

        Returns:
            Absolute path to the generated DOCX file.
        """
        validated_input = self._validate_path(input_path)
        validated_output = self._validate_path(output_path)

        if not validated_input.exists():
            raise FileNotFoundError(f"Input file not found: {input_path}")

        cv = PdfConverter(str(validated_input))
        try:
            cv.convert(str(validated_output))
        finally:
            cv.close()

        return str(validated_output)
