import os
import shutil
import zipfile
import tempfile
import subprocess
import uuid
from typing import List, Optional, Dict
from fastapi import APIRouter, UploadFile, File, HTTPException
from pydantic import BaseModel
import aiohttp
import asyncio
import xml.etree.ElementTree as ET
import re

from services.documents_loader import DocumentsLoader
from utils.asset_directory_utils import get_images_directory
import uuid
from constants.documents import POWERPOINT_TYPES


PPTX_SLIDES_ROUTER = APIRouter(prefix="/pptx-slides", tags=["PPTX Slides"])


class SlideData(BaseModel):
    slide_number: int
    screenshot_url: str
    xml_content: str
    normalized_fonts: List[str]


class FontAnalysisResult(BaseModel):
    internally_supported_fonts: List[
        Dict[str, str]
    ]  # [{"name": "Open Sans", "google_fonts_url": "..."}]
    not_supported_fonts: List[str]  # ["Custom Font Name"]


class PptxSlidesResponse(BaseModel):
    success: bool
    slides: List[SlideData]
    total_slides: int
    fonts: Optional[FontAnalysisResult] = None


# NEW: Fonts-only router and response for PPTX
class PptxFontsResponse(BaseModel):
    success: bool
    fonts: FontAnalysisResult


PPTX_FONTS_ROUTER = APIRouter(prefix="/pptx-fonts", tags=["PPTX Fonts"])

# NEW: Normalize font family names by removing style/weight/stretch descriptors and splitting camel case
_STYLE_TOKENS = {
    # styles
    "italic",
    "italics",
    "ital",
    "oblique",
    "roman",
    # combined style shortcuts
    "bolditalic",
    "bolditalics",
    # weights
    "thin",
    "hairline",
    "extralight",
    "ultralight",
    "light",
    "demilight",
    "semilight",
    "book",
    "regular",
    "normal",
    "medium",
    "semibold",
    "demibold",
    "bold",
    "extrabold",
    "ultrabold",
    "black",
    "extrablack",
    "ultrablack",
    "heavy",
    # width/stretch
    "narrow",
    "condensed",
    "semicondensed",
    "extracondensed",
    "ultracondensed",
    "expanded",
    "semiexpanded",
    "extraexpanded",
    "ultraexpanded",
}
# Modifiers commonly used with style tokens
_STYLE_MODIFIERS = {"semi", "demi", "extra", "ultra"}


def _insert_spaces_in_camel_case(value: str) -> str:
    # Insert space before capital letters preceded by lowercase or digits (e.g., MontserratBold -> Montserrat Bold)
    value = re.sub(r"(?<=[a-z0-9])([A-Z])", r" \1", value)
    # Handle sequences like BoldItalic -> Bold Italic
    value = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1 \2", value)
    return value


def normalize_font_family_name(raw_name: str) -> str:
    if not raw_name:
        return raw_name
    # Replace separators with spaces
    name = raw_name.replace("_", " ").replace("-", " ")
    # Insert spaces in camel case
    name = _insert_spaces_in_camel_case(name)
    # Collapse multiple spaces
    name = re.sub(r"\s+", " ", name).strip()
    # Lowercase helper for matching but keep original casing for output
    lower_name = name.lower()
    # Quick cut: if the full string ends with a pure style suffix, trim it
    for style in sorted(_STYLE_TOKENS, key=len, reverse=True):
        if lower_name.endswith(" " + style):
            name = name[: -(len(style) + 1)]
            lower_name = lower_name[: -(len(style) + 1)]
            break
    # Tokenize
    tokens_original = name.split(" ")
    tokens_filtered: List[str] = []
    for index, tok in enumerate(tokens_original):
        lower_tok = tok.lower()
        # Always keep the first token to avoid stripping families like "Black Ops One"
        if index == 0:
            tokens_filtered.append(tok)
            continue
        # Drop style tokens and standalone modifiers
        if lower_tok in _STYLE_TOKENS or lower_tok in _STYLE_MODIFIERS:
            continue
        tokens_filtered.append(tok)
    # If everything except first token was dropped and first token is a style token (unlikely), fallback to original
    if not tokens_filtered:
        tokens_filtered = tokens_original
    normalized = " ".join(tokens_filtered).strip()
    # Final cleanup of leftover multiple spaces
    normalized = re.sub(r"\s+", " ", normalized)
    return normalized


def extract_fonts_from_oxml(xml_content: str) -> List[str]:
    """
    Extract font names from OXML content.

    Args:
        xml_content: OXML content as string

    Returns:
        List of unique font names found in the OXML
    """
    fonts = set()

    try:
        # Parse the XML content
        root = ET.fromstring(xml_content)

        # Define namespaces commonly used in OXML
        namespaces = {
            "a": "http://schemas.openxmlformats.org/drawingml/2006/main",
            "p": "http://schemas.openxmlformats.org/presentationml/2006/main",
            "r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
        }

        # Search for font references in various OXML elements
        # Look for latin fonts
        for font_elem in root.findall(".//a:latin", namespaces):
            if "typeface" in font_elem.attrib:
                fonts.add(font_elem.attrib["typeface"])

        # Look for east asian fonts
        for font_elem in root.findall(".//a:ea", namespaces):
            if "typeface" in font_elem.attrib:
                fonts.add(font_elem.attrib["typeface"])

        # Look for complex script fonts
        for font_elem in root.findall(".//a:cs", namespaces):
            if "typeface" in font_elem.attrib:
                fonts.add(font_elem.attrib["typeface"])

        # Look for font references in theme elements
        for font_elem in root.findall(".//a:font", namespaces):
            if "typeface" in font_elem.attrib:
                fonts.add(font_elem.attrib["typeface"])

        # Look for rPr (run properties) font references
        for rpr_elem in root.findall(".//a:rPr", namespaces):
            for font_elem in rpr_elem.findall(".//a:latin", namespaces):
                if "typeface" in font_elem.attrib:
                    fonts.add(font_elem.attrib["typeface"])

        # Also search without namespace prefix for compatibility
        for font_elem in root.findall(".//latin"):
            if "typeface" in font_elem.attrib:
                fonts.add(font_elem.attrib["typeface"])

        # Regex fallback for fonts that might be missed
        font_pattern = r'typeface="([^"]+)"'
        regex_fonts = re.findall(font_pattern, xml_content)
        fonts.update(regex_fonts)

        # Filter out system fonts and empty values
        system_fonts = {"+mn-lt", "+mj-lt", "+mn-ea", "+mj-ea", "+mn-cs", "+mj-cs", ""}
        fonts = {font for font in fonts if font not in system_fonts and font.strip()}

        return list(fonts)

    except Exception as e:
        print(f"Error extracting fonts from OXML: {e}")
        return []


async def check_google_font_availability(font_name: str) -> bool:
    """
    Check if a font is available in Google Fonts.

    Args:
        font_name: Name of the font to check

    Returns:
        True if font is available in Google Fonts, False otherwise
    """
    try:
        formatted_name = font_name.replace(" ", "+")
        url = f"https://fonts.googleapis.com/css2?family={formatted_name}&display=swap"

        async with aiohttp.ClientSession() as session:
            async with session.head(
                url, timeout=aiohttp.ClientTimeout(total=10)
            ) as response:
                return response.status == 200

    except Exception as e:
        print(f"Error checking Google Font availability for {font_name}: {e}")
        return False


async def analyze_fonts_in_all_slides(slide_xmls: List[str]) -> FontAnalysisResult:
    """
    Analyze fonts across all slides and determine Google Fonts availability.

    Args:
        slide_xmls: List of OXML content strings from all slides

    Returns:
        FontAnalysisResult with supported and unsupported fonts
    """
    # Extract fonts from all slides
    raw_fonts = set()
    for xml_content in slide_xmls:
        slide_fonts = extract_fonts_from_oxml(xml_content)
        raw_fonts.update(slide_fonts)

    # Normalize to root families (e.g., "Montserrat Italic" -> "Montserrat")
    normalized_fonts = {normalize_font_family_name(f) for f in raw_fonts}
    # Remove empties if any
    normalized_fonts = {f for f in normalized_fonts if f}

    if not normalized_fonts:
        return FontAnalysisResult(internally_supported_fonts=[], not_supported_fonts=[])

    # Check each normalized font's availability in Google Fonts concurrently
    tasks = [check_google_font_availability(font) for font in normalized_fonts]
    results = await asyncio.gather(*tasks)

    internally_supported_fonts = []
    not_supported_fonts = []

    for font, is_available in zip(normalized_fonts, results):
        if is_available:
            formatted_name = font.replace(" ", "+")
            google_fonts_url = f"https://fonts.googleapis.com/css2?family={formatted_name}&display=swap"
            internally_supported_fonts.append(
                {"name": font, "google_fonts_url": google_fonts_url}
            )
        else:
            not_supported_fonts.append(font)

    return FontAnalysisResult(
        internally_supported_fonts=internally_supported_fonts, not_supported_fonts=[]
    )


@PPTX_SLIDES_ROUTER.post("/process", response_model=PptxSlidesResponse)
async def process_pptx_slides(
    pptx_file: UploadFile = File(..., description="PPTX file to process"),
    fonts: Optional[List[UploadFile]] = File(None, description="Optional font files"),
):
    """
    Process a PPTX file to extract slide screenshots and XML content.

    This endpoint:
    1. Validates the uploaded PPTX file
    2. Installs any provided font files
    3. Unzips the PPTX to extract slide XMLs
    4. Uses LibreOffice to generate slide screenshots
    5. Returns both screenshot URLs and XML content for each slide
    """

    # Validate PPTX file
    if pptx_file.content_type not in POWERPOINT_TYPES:
        raise HTTPException(
            status_code=400,
            detail=f"Invalid file type. Expected PPTX file, got {pptx_file.content_type}",
        )
    # Enforce 100MB size limit
    if (
        hasattr(pptx_file, "size")
        and pptx_file.size
        and pptx_file.size > (100 * 1024 * 1024)
    ):
        raise HTTPException(
            status_code=400,
            detail="PPTX file exceeded max upload size of 100 MB",
        )

    # Create temporary directory for processing
    with tempfile.TemporaryDirectory() as temp_dir:
        if True:
            # Save uploaded PPTX file
            pptx_path = os.path.join(temp_dir, "presentation.pptx")
            with open(pptx_path, "wb") as f:
                pptx_content = await pptx_file.read()
                f.write(pptx_content)

            # Install fonts if provided
            if fonts:
                await _install_fonts(fonts, temp_dir)

            # Extract slide XMLs from PPTX
            slide_xmls = _extract_slide_xmls(pptx_path, temp_dir)

            # Convert PPTX to PDF
            pdf_path = await _convert_pptx_to_pdf(pptx_path, temp_dir)

            # Generate screenshots using LibreOffice
            screenshot_paths = await DocumentsLoader.get_page_images_from_pdf_async(
                pdf_path, temp_dir
            )
            print(f"Screenshot paths: {screenshot_paths}")

            # Analyze fonts across all slides
            font_analysis = await analyze_fonts_in_all_slides(slide_xmls)
            print(
                f"Font analysis completed: {len(font_analysis.internally_supported_fonts)} supported, {len(font_analysis.not_supported_fonts)} not supported"
            )

            # Move screenshots to images directory and generate URLs
            images_dir = get_images_directory()
            presentation_id = uuid.uuid4()
            presentation_images_dir = os.path.join(images_dir, str(presentation_id))
            os.makedirs(presentation_images_dir, exist_ok=True)

            slides_data = []

            for i, (xml_content, screenshot_path) in enumerate(
                zip(slide_xmls, screenshot_paths), 1
            ):
                # Move screenshot to permanent location
                screenshot_filename = f"slide_{i}.png"
                permanent_screenshot_path = os.path.join(
                    presentation_images_dir, screenshot_filename
                )

                if (
                    os.path.exists(screenshot_path)
                    and os.path.getsize(screenshot_path) > 0
                ):
                    # Use shutil.copy2 instead of os.rename to handle cross-device moves
                    shutil.copy2(screenshot_path, permanent_screenshot_path)
                    screenshot_url = (
                        f"/app_data/images/{presentation_id}/{screenshot_filename}"
                    )
                else:
                    # Fallback if screenshot generation failed or file is empty placeholder
                    screenshot_url = "/static/images/placeholder.jpg"

                # Compute normalized fonts for this slide
                raw_slide_fonts = extract_fonts_from_oxml(xml_content)
                normalized_fonts = sorted(
                    {normalize_font_family_name(f) for f in raw_slide_fonts if f}
                )

                slides_data.append(
                    SlideData(
                        slide_number=i,
                        screenshot_url=screenshot_url,
                        xml_content=xml_content,
                        normalized_fonts=normalized_fonts,
                    )
                )

            return PptxSlidesResponse(
                success=True,
                slides=slides_data,
                total_slides=len(slides_data),
                fonts=font_analysis,
            )


# NEW: Fonts-only endpoint leveraging the same font extraction/analysis
@PPTX_FONTS_ROUTER.post("/process", response_model=PptxFontsResponse)
async def process_pptx_fonts(
    pptx_file: UploadFile = File(..., description="PPTX file to analyze fonts from")
):
    """
    Analyze a PPTX file and return only the fonts used in the document.

    Uses the exact same font extraction and analysis utilities as the /pptx-slides endpoint.
    """
    # Validate PPTX file
    if pptx_file.content_type not in POWERPOINT_TYPES:
        raise HTTPException(
            status_code=400,
            detail=f"Invalid file type. Expected PPTX file, got {pptx_file.content_type}",
        )

    # Create temporary directory for processing
    with tempfile.TemporaryDirectory() as temp_dir:
        # Save uploaded PPTX file
        pptx_path = os.path.join(temp_dir, "presentation.pptx")
        with open(pptx_path, "wb") as f:
            pptx_content = await pptx_file.read()
            f.write(pptx_content)

        # Extract slide XMLs from PPTX
        slide_xmls = _extract_slide_xmls(pptx_path, temp_dir)

        # Analyze fonts across all slides (same logic as in /pptx-slides)
        font_analysis = await analyze_fonts_in_all_slides(slide_xmls)

        return PptxFontsResponse(
            success=True,
            fonts=font_analysis,
        )


def _create_font_alias_config(raw_fonts: List[str]) -> str:
    """Create a temporary fontconfig configuration that aliases variant family names to normalized root families.
    Returns the path to the config file.
    """
    # Build mapping from raw -> normalized where different
    mappings: Dict[str, str] = {}
    for f in raw_fonts:
        normalized = normalize_font_family_name(f)
        if normalized and normalized != f:
            mappings[f] = normalized
    # Create config only if we have mappings
    fd, fonts_conf_path = tempfile.mkstemp(prefix="fonts_alias_", suffix=".conf")
    os.close(fd)
    with open(fonts_conf_path, "w", encoding="utf-8") as cfg:
        cfg.write(
            """<?xml version='1.0'?>
<!DOCTYPE fontconfig SYSTEM "urn:fontconfig:fonts.dtd">
<fontconfig>
  <include>/etc/fonts/fonts.conf</include>
"""
        )
        for src, dst in mappings.items():
            cfg.write(
                f"""
  <match target="pattern">
    <test name="family" compare="eq">
      <string>{src}</string>
    </test>
    <edit name="family" mode="assign" binding="strong">
      <string>{dst}</string>
    </edit>
  </match>
"""
            )
        cfg.write("\n</fontconfig>\n")
    return fonts_conf_path


async def _install_fonts(fonts: List[UploadFile], temp_dir: str) -> None:
    """Install provided font files to the system."""
    fonts_dir = os.path.join(temp_dir, "fonts")
    os.makedirs(fonts_dir, exist_ok=True)

    for font_file in fonts:
        # Save font file
        font_path = os.path.join(fonts_dir, font_file.filename)
        with open(font_path, "wb") as f:
            font_content = await font_file.read()
            f.write(font_content)

        # Install font (copy to system fonts directory)
        try:
            subprocess.run(
                ["cp", font_path, "/usr/share/fonts/truetype/"],
                check=True,
                capture_output=True,
            )
        except subprocess.CalledProcessError as e:
            print(f"Warning: Failed to install font {font_file.filename}: {e}")

    # Refresh font cache
    try:
        subprocess.run(["fc-cache", "-f", "-v"], check=True, capture_output=True)
    except subprocess.CalledProcessError as e:
        print(f"Warning: Failed to refresh font cache: {e}")


def _extract_slide_xmls(pptx_path: str, temp_dir: str) -> List[str]:
    """Extract slide XML content from PPTX file."""
    slide_xmls = []
    extract_dir = os.path.join(temp_dir, "pptx_extract")

    try:
        # Unzip PPTX file
        with zipfile.ZipFile(pptx_path, "r") as zip_ref:
            zip_ref.extractall(extract_dir)

        # Look for slides in ppt/slides/ directory
        slides_dir = os.path.join(extract_dir, "ppt", "slides")

        if not os.path.exists(slides_dir):
            raise Exception("No slides directory found in PPTX file")

        # Get all slide XML files and sort them numerically
        slide_files = [
            f
            for f in os.listdir(slides_dir)
            if f.startswith("slide") and f.endswith(".xml")
        ]
        slide_files.sort(key=lambda x: int(x.replace("slide", "").replace(".xml", "")))

        # Read XML content from each slide
        for slide_file in slide_files:
            slide_path = os.path.join(slides_dir, slide_file)
            with open(slide_path, "r", encoding="utf-8") as f:
                slide_xmls.append(f.read())

        return slide_xmls

    except Exception as e:
        raise Exception(f"Failed to extract slide XMLs: {str(e)}")


async def _convert_pptx_to_pdf(pptx_path: str, temp_dir: str) -> str:
    """Generate PNG screenshots of PPTX slides using LibreOffice + ImageMagick."""
    screenshots_dir = os.path.join(temp_dir, "screenshots")
    os.makedirs(screenshots_dir, exist_ok=True)

    try:
        # First, get the number of slides by extracting XMLs
        slide_xmls = _extract_slide_xmls(pptx_path, temp_dir)
        slide_count = len(slide_xmls)

        # Build font alias config to force variant families to resolve to normalized root families
        raw_fonts: List[str] = []
        for xml in slide_xmls:
            raw_fonts.extend(extract_fonts_from_oxml(xml))
        raw_fonts = list({f for f in raw_fonts if f})
        fonts_conf_path = _create_font_alias_config(raw_fonts)
        env = os.environ.copy()
        env["FONTCONFIG_FILE"] = fonts_conf_path

        print(f"Found {slide_count} slides in presentation")

        # Step 1: Convert PPTX to PDF using LibreOffice
        print("Starting LibreOffice PDF conversion...")
        pdf_filename = "temp_presentation.pdf"
        pdf_path = os.path.join(screenshots_dir, pdf_filename)

        try:
            result = subprocess.run(
                [
                    "libreoffice",
                    "--headless",
                    "--convert-to",
                    "pdf",
                    "--outdir",
                    screenshots_dir,
                    pptx_path,
                ],
                check=True,
                capture_output=True,
                text=True,
                timeout=500,
                env=env,
            )

            print(f"LibreOffice PDF conversion output: {result.stdout}")
            if result.stderr:
                print(f"LibreOffice PDF conversion warnings: {result.stderr}")
        except subprocess.TimeoutExpired:
            raise Exception("LibreOffice PDF conversion timed out after 120 seconds")
        except subprocess.CalledProcessError as e:
            error_msg = e.stderr if e.stderr else str(e)
            raise Exception(f"LibreOffice PDF conversion failed: {error_msg}")

        # Find the generated PDF file (LibreOffice uses original filename)
        pdf_files = [f for f in os.listdir(screenshots_dir) if f.endswith(".pdf")]
        if not pdf_files:
            raise Exception("LibreOffice failed to generate PDF file")

        actual_pdf_path = os.path.join(screenshots_dir, pdf_files[0])
        print(f"Generated PDF: {actual_pdf_path}")
        return actual_pdf_path

    except Exception as e:
        # Re-raise the specific exceptions we've already handled
        if "timed out" in str(e) or "failed:" in str(e):
            raise
        # Handle any other unexpected exceptions
        raise Exception(f"Screenshot generation failed: {str(e)}")
