import io
import os
import sys
import zipfile
import json
import csv
import hashlib
import difflib
from typing import List, Optional
from dotenv import load_dotenv
from fastapi import FastAPI, UploadFile, File, Form, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse, JSONResponse, PlainTextResponse, Response
from starlette.types import ASGIApp, Scope, Receive, Send
from PIL import Image, ImageEnhance, ImageFilter, ImageOps, ImageDraw, ImageFont
import pypdf
import pypdfium2 as pdfium
import openpyxl
import pandas as pd
import docx
import qrcode
from reportlab.lib.pagesizes import letter
from reportlab.pdfgen import canvas
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle

# Load environment configuration (.env from backend or root)
_base_dir = os.path.dirname(os.path.abspath(__file__))
load_dotenv(os.path.join(_base_dir, "..", ".env"))
load_dotenv(os.path.join(_base_dir, ".env"))
load_dotenv()

SITE_URL = os.getenv("SITE_URL", "https://brotools.com").rstrip("/")
API_URL = os.getenv("API_URL", "http://127.0.0.1:8000").rstrip("/")
cors_origins_raw = os.getenv("CORS_ORIGINS", "*").strip()

if cors_origins_raw == "*":
    allow_origins = ["*"]
else:
    allow_origins = [orig.strip() for orig in cors_origins_raw.split(",") if orig.strip()]
    if SITE_URL and SITE_URL not in allow_origins and "*" not in allow_origins:
        allow_origins.append(SITE_URL)

app = FastAPI(
    title="BroTools Core API",
    description="High-performance document, PDF, image processing, and converter engine for BroTools.",
    version="1.0.0"
)

# Enable CORS for React frontend (Vite default :5173, production domain, or configured origins)
app.add_middleware(
    CORSMiddleware,
    allow_origins=allow_origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


class RoutePrefixNormalizerMiddleware:
    """
    Normalizes routes so that both '/api/endpoint' and '/endpoint' function identically.
    This seamlessly handles:
    1. Subdomains (e.g. api.yourdomain.com/api/... or api.yourdomain.com/...)
    2. Subpath mounts (e.g. yourdomain.com/api where Phusion Passenger strips /api into root_path)
    3. Reverse proxy and rewrite configurations.
    """
    def __init__(self, app: ASGIApp):
        self.app = app

    async def __call__(self, scope: Scope, receive: Receive, send: Send):
        if scope["type"] == "http":
            path = scope.get("path", "")
            passthrough_paths = {
                "/", "", "/robots.txt", "/sitemap.xml",
                "/docs", "/redoc", "/openapi.json"
            }
            if not path.startswith("/api") and path not in passthrough_paths:
                scope["path"] = "/api" + (path if path.startswith("/") else "/" + path)
        await self.app(scope, receive, send)


app.add_middleware(RoutePrefixNormalizerMiddleware)


@app.get("/")
def api_root():
    """Friendly root endpoint confirming API is running and operational."""
    return {
        "status": "online",
        "service": "BroTools Core API",
        "version": "1.0.0",
        "site_url": SITE_URL,
        "api_url": API_URL,
        "endpoints": {
            "health": "/api/health",
            "diagnostics": "/api/diagnostics",
            "docs": "/docs"
        }
    }


@app.get("/api/diagnostics")
def api_diagnostics():
    """Verify that all cPanel dependencies and libraries are loaded and operational."""
    modules_status = {}

    def test_import(mod_name, get_ver=None):
        try:
            mod = __import__(mod_name)
            if get_ver:
                modules_status[mod_name] = get_ver(mod)
            else:
                modules_status[mod_name] = getattr(mod, "__version__", "installed")
        except Exception as e:
            modules_status[mod_name] = f"error: {str(e)}"

    test_import("pypdf")
    test_import("pypdfium2")
    test_import("PIL", lambda m: getattr(m, "__version__", "installed"))
    test_import("reportlab")
    test_import("docx")
    test_import("openpyxl")
    test_import("pandas")
    test_import("qrcode")
    test_import("a2wsgi")

    missing = [k for k, v in modules_status.items() if str(v).startswith("error")]
    all_ready = len(missing) == 0

    return {
        "status": "ready" if all_ready else "degraded",
        "python_version": sys.version,
        "platform": sys.platform,
        "modules": modules_status,
        "missing_modules": missing,
        "environment": {
            "site_url": SITE_URL,
            "api_url": API_URL,
            "cors_origins": allow_origins
        },
        "cpanel_guidance": (
            "All core modules are verified and ready!"
            if all_ready
            else f"Missing modules: {', '.join(missing)}. In cPanel 'Setup Python App', add requirements.txt to Configuration files and click 'Run Pip Install'."
        )
    }


@app.get("/api/health")
def health_check():
    return {
        "status": "online",
        "service": "BroTools Python Engine",
        "site_url": SITE_URL,
        "api_url": API_URL,
        "cors_origins": allow_origins,
        "features": [
            "pdf_merge", "pdf_split", "pdf_rotate", "pdf_watermark",
            "pdf_protect", "pdf_unlock", "pdf_to_images", "pdf_from_images",
            "pdf_extract_text", "image_convert", "image_compress", "image_resize",
            "image_crop", "image_filter", "image_metadata", "docx_to_text",
            "docx_to_pdf", "csv_excel_converter", "qr_generator", "hash_calculator", "text_diff"
        ]
    }

@app.get("/robots.txt", response_class=PlainTextResponse)
def get_robots_txt():
    """Fallback robots.txt for API domain"""
    return f"""User-agent: *
Allow: /
Disallow: /api/

Sitemap: {SITE_URL}/sitemap.xml
"""

@app.get("/sitemap.xml")
def get_sitemap_xml():
    """API fallback link redirecting search crawlers to main site sitemap"""
    content = f"""<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>{SITE_URL}/</loc>
    <changefreq>daily</changefreq>
    <priority>1.0</priority>
  </url>
  <url>
    <loc>{SITE_URL}/sitemap.xml</loc>
    <changefreq>weekly</changefreq>
    <priority>0.8</priority>
  </url>
</urlset>"""
    return Response(content=content, media_type="application/xml")

# ═══════════════════════════════════════════════════════════════════
# PDF TOOLS
# ═══════════════════════════════════════════════════════════════════

@app.post("/api/pdf/merge")
async def merge_pdfs(files: List[UploadFile] = File(...)):
    """Merge multiple uploaded PDF files into a single ordered PDF."""
    if len(files) < 2:
        raise HTTPException(status_code=400, detail="At least 2 PDF files are required for merging.")
    
    merger = pypdf.PdfWriter()
    try:
        for file in files:
            content = await file.read()
            stream = io.BytesIO(content)
            merger.append(stream)
        
        output = io.BytesIO()
        merger.write(output)
        merger.close()
        output.seek(0)
        return StreamingResponse(
            output,
            media_type="application/pdf",
            headers={"Content-Disposition": "attachment; filename=brotools_merged.pdf"}
        )
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Failed to merge PDFs: {str(e)}")

@app.post("/api/pdf/split")
async def split_pdf(
    file: UploadFile = File(...),
    page_ranges: str = Form(..., description="e.g. '1-3, 5, 7-10' or 'all'")
):
    """Split PDF into selected pages or zip of individual pages."""
    content = await file.read()
    reader = pypdf.PdfReader(io.BytesIO(content))
    total_pages = len(reader.pages)

    if total_pages == 0:
        raise HTTPException(status_code=400, detail="The uploaded PDF contains no pages.")

    try:
        if page_ranges.strip().lower() == "all":
            # Zip all individual pages
            zip_buffer = io.BytesIO()
            with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zip_file:
                for idx, page in enumerate(reader.pages):
                    writer = pypdf.PdfWriter()
                    writer.add_page(page)
                    page_out = io.BytesIO()
                    writer.write(page_out)
                    page_out.seek(0)
                    zip_file.writestr(f"page_{idx+1}.pdf", page_out.getvalue())
            zip_buffer.seek(0)
            return StreamingResponse(
                zip_buffer,
                media_type="application/zip",
                headers={"Content-Disposition": "attachment; filename=split_pages.zip"}
            )
        else:
            # Extract specific ranges into a single new PDF
            writer = pypdf.PdfWriter()
            selected_indices = set()
            parts = [p.strip() for p in page_ranges.split(",") if p.strip()]
            for part in parts:
                if "-" in part:
                    start_str, end_str = part.split("-", 1)
                    start = max(1, int(start_str.strip()))
                    end = min(total_pages, int(end_str.strip()))
                    for p_num in range(start, end + 1):
                        selected_indices.add(p_num - 1)
                else:
                    p_num = int(part)
                    if 1 <= p_num <= total_pages:
                        selected_indices.add(p_num - 1)

            for idx in sorted(selected_indices):
                writer.add_page(reader.pages[idx])

            output = io.BytesIO()
            writer.write(output)
            output.seek(0)
            return StreamingResponse(
                output,
                media_type="application/pdf",
                headers={"Content-Disposition": "attachment; filename=extracted_pages.pdf"}
            )
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Failed to split PDF: {str(e)}")

@app.post("/api/pdf/rotate")
async def rotate_pdf(
    file: UploadFile = File(...),
    angle: int = Form(90, description="Angle: 90, 180, 270"),
    pages: str = Form("all", description="'all', 'odd', 'even', or '1,3,5'")
):
    """Rotate PDF pages by specified angle."""
    if angle not in [90, 180, 270]:
        raise HTTPException(status_code=400, detail="Angle must be 90, 180, or 270.")
    
    content = await file.read()
    reader = pypdf.PdfReader(io.BytesIO(content))
    writer = pypdf.PdfWriter()

    target_indices = set()
    total = len(reader.pages)
    if pages == "all":
        target_indices = set(range(total))
    elif pages == "odd":
        target_indices = {i for i in range(total) if (i + 1) % 2 != 0}
    elif pages == "even":
        target_indices = {i for i in range(total) if (i + 1) % 2 == 0}
    else:
        for p in pages.split(","):
            if p.strip().isdigit():
                idx = int(p.strip()) - 1
                if 0 <= idx < total:
                    target_indices.add(idx)

    for idx, page in enumerate(reader.pages):
        if idx in target_indices:
            page.rotate(angle)
        writer.add_page(page)

    output = io.BytesIO()
    writer.write(output)
    output.seek(0)
    return StreamingResponse(
        output,
        media_type="application/pdf",
        headers={"Content-Disposition": "attachment; filename=rotated.pdf"}
    )

@app.post("/api/pdf/watermark")
async def watermark_pdf(
    file: UploadFile = File(...),
    text: str = Form("CONFIDENTIAL"),
    opacity: float = Form(0.25),
    font_size: int = Form(48)
):
    """Add a diagonal watermark text overlay to every page in the PDF."""
    content = await file.read()
    reader = pypdf.PdfReader(io.BytesIO(content))
    writer = pypdf.PdfWriter()

    # Generate watermark overlay PDF with ReportLab
    wm_buffer = io.BytesIO()
    first_page = reader.pages[0]
    w = float(first_page.mediabox.width)
    h = float(first_page.mediabox.height)

    can = canvas.Canvas(wm_buffer, pagesize=(w, h))
    can.saveState()
    can.setFont("Helvetica-Bold", font_size)
    can.setFillColorRGB(0.5, 0.5, 0.5, alpha=opacity)
    can.translate(w / 2, h / 2)
    can.rotate(45)
    can.drawCentredString(0, 0, text)
    can.restoreState()
    can.save()
    wm_buffer.seek(0)

    wm_reader = pypdf.PdfReader(wm_buffer)
    watermark_page = wm_reader.pages[0]

    for page in reader.pages:
        page.merge_page(watermark_page)
        writer.add_page(page)

    output = io.BytesIO()
    writer.write(output)
    output.seek(0)
    return StreamingResponse(
        output,
        media_type="application/pdf",
        headers={"Content-Disposition": "attachment; filename=watermarked.pdf"}
    )

@app.post("/api/pdf/protect")
async def protect_pdf(
    file: UploadFile = File(...),
    password: str = Form(...)
):
    """Encrypt and password protect a PDF."""
    if not password:
        raise HTTPException(status_code=400, detail="Password cannot be empty.")
    
    content = await file.read()
    reader = pypdf.PdfReader(io.BytesIO(content))
    writer = pypdf.PdfWriter()
    for page in reader.pages:
        writer.add_page(page)
    
    writer.encrypt(password)
    output = io.BytesIO()
    writer.write(output)
    output.seek(0)
    return StreamingResponse(
        output,
        media_type="application/pdf",
        headers={"Content-Disposition": "attachment; filename=protected.pdf"}
    )

@app.post("/api/pdf/unlock")
async def unlock_pdf(
    file: UploadFile = File(...),
    password: str = Form(...)
):
    """Decrypt a password-protected PDF."""
    content = await file.read()
    reader = pypdf.PdfReader(io.BytesIO(content))
    if reader.is_encrypted:
        success = reader.decrypt(password)
        if not success:
            raise HTTPException(status_code=400, detail="Incorrect password or unable to decrypt.")
    
    writer = pypdf.PdfWriter()
    for page in reader.pages:
        writer.add_page(page)
    
    output = io.BytesIO()
    writer.write(output)
    output.seek(0)
    return StreamingResponse(
        output,
        media_type="application/pdf",
        headers={"Content-Disposition": "attachment; filename=unlocked.pdf"}
    )

@app.post("/api/pdf/extract-text")
async def extract_pdf_text(file: UploadFile = File(...)):
    """Extract full text and metadata from PDF."""
    content = await file.read()
    reader = pypdf.PdfReader(io.BytesIO(content))
    
    meta = {}
    if reader.metadata:
        for k, v in reader.metadata.items():
            meta[k.replace("/", "")] = str(v)

    pages_text = []
    full_text = []
    for idx, page in enumerate(reader.pages):
        text = page.extract_text() or ""
        pages_text.append({"page": idx + 1, "text": text, "char_count": len(text)})
        full_text.append(text)

    return {
        "filename": file.filename,
        "total_pages": len(reader.pages),
        "metadata": meta,
        "total_characters": sum(p["char_count"] for p in pages_text),
        "pages": pages_text,
        "full_text": "\n\n--- PAGE BREAK ---\n\n".join(full_text)
    }

@app.post("/api/pdf/to-images")
async def pdf_to_images(
    file: UploadFile = File(...),
    img_format: str = Form("png"),
    dpi: int = Form(150)
):
    """Render each page of a PDF into high-res images and package as a ZIP."""
    content = await file.read()
    pdf = pdfium.PdfDocument(content)
    scale = dpi / 72.0

    zip_buffer = io.BytesIO()
    fmt = "PNG" if img_format.lower() == "png" else "JPEG"
    ext = "png" if fmt == "PNG" else "jpg"

    with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zip_file:
        for i, page in enumerate(pdf):
            image = page.render(scale=scale).to_pil()
            img_io = io.BytesIO()
            if fmt == "JPEG":
                image = image.convert("RGB")
                image.save(img_io, format="JPEG", quality=90)
            else:
                image.save(img_io, format="PNG")
            img_io.seek(0)
            zip_file.writestr(f"page_{i+1:03d}.{ext}", img_io.getvalue())

    zip_buffer.seek(0)
    return StreamingResponse(
        zip_buffer,
        media_type="application/zip",
        headers={"Content-Disposition": f"attachment; filename=pdf_images_{ext}.zip"}
    )

@app.post("/api/pdf/from-images")
async def images_to_pdf(files: List[UploadFile] = File(...)):
    """Convert multiple images into a unified PDF document."""
    if not files:
        raise HTTPException(status_code=400, detail="At least one image is required.")

    pil_images = []
    for f in files:
        data = await f.read()
        try:
            im = Image.open(io.BytesIO(data))
            if im.mode in ("RGBA", "P"):
                im = im.convert("RGB")
            pil_images.append(im)
        except Exception as e:
            continue

    if not pil_images:
        raise HTTPException(status_code=400, detail="No valid images were provided.")

    output = io.BytesIO()
    first_image = pil_images[0]
    rest_images = pil_images[1:] if len(pil_images) > 1 else []
    first_image.save(output, format="PDF", save_all=True, append_images=rest_images)
    output.seek(0)

    return StreamingResponse(
        output,
        media_type="application/pdf",
        headers={"Content-Disposition": "attachment; filename=images_document.pdf"}
    )

# ═══════════════════════════════════════════════════════════════════
# IMAGE TOOLS
# ═══════════════════════════════════════════════════════════════════

@app.post("/api/image/convert")
async def convert_image(
    file: UploadFile = File(...),
    target_format: str = Form("webp"),
    quality: int = Form(85)
):
    """Convert image to WEBP, PNG, JPEG, BMP, ICO, TIFF."""
    content = await file.read()
    im = Image.open(io.BytesIO(content))
    target_format = target_format.upper()
    if target_format == "JPG":
        target_format = "JPEG"

    output = io.BytesIO()
    if target_format in ("JPEG", "BMP") and im.mode in ("RGBA", "LA", "P"):
        background = Image.new("RGB", im.size, (255, 255, 255))
        background.paste(im, mask=im.split()[-1] if im.mode == "RGBA" else None)
        im = background
    elif target_format == "ICO":
        im = im.resize((64, 64), Image.Resampling.LANCZOS)

    save_args = {"format": target_format}
    if target_format in ("JPEG", "WEBP"):
        save_args["quality"] = quality

    im.save(output, **save_args)
    output.seek(0)

    ext = target_format.lower()
    mimes = {
        "JPEG": "image/jpeg",
        "PNG": "image/png",
        "WEBP": "image/webp",
        "BMP": "image/bmp",
        "ICO": "image/x-icon",
        "TIFF": "image/tiff"
    }
    return StreamingResponse(
        output,
        media_type=mimes.get(target_format, "application/octet-stream"),
        headers={"Content-Disposition": f"attachment; filename=converted.{ext}"}
    )

@app.post("/api/image/compress")
async def compress_image(
    file: UploadFile = File(...),
    quality: int = Form(60),
    max_width: Optional[int] = Form(None)
):
    """Compress image with custom quality & dimensions, reporting byte size reduction."""
    original_data = await file.read()
    orig_size = len(original_data)
    im = Image.open(io.BytesIO(original_data))

    if max_width and im.width > max_width:
        ratio = max_width / float(im.width)
        new_height = int(float(im.height) * float(ratio))
        im = im.resize((max_width, new_height), Image.Resampling.LANCZOS)

    fmt = im.format or "JPEG"
    if fmt not in ("JPEG", "PNG", "WEBP"):
        fmt = "JPEG"

    output = io.BytesIO()
    if fmt == "JPEG" and im.mode in ("RGBA", "LA", "P"):
        bg = Image.new("RGB", im.size, (255, 255, 255))
        bg.paste(im, mask=im.split()[-1] if im.mode == "RGBA" else None)
        im = bg

    if fmt in ("JPEG", "WEBP"):
        im.save(output, format=fmt, quality=quality, optimize=True)
    else:
        im.save(output, format=fmt, optimize=True)

    output.seek(0)
    compressed_bytes = output.getvalue()
    new_size = len(compressed_bytes)
    saved_percent = round((1.0 - (new_size / orig_size)) * 100, 1) if orig_size > 0 else 0

    return StreamingResponse(
        io.BytesIO(compressed_bytes),
        media_type=f"image/{fmt.lower()}",
        headers={
            "Content-Disposition": f"attachment; filename=compressed_{file.filename}",
            "X-Original-Size": str(orig_size),
            "X-Compressed-Size": str(new_size),
            "X-Saved-Percent": str(saved_percent)
        }
    )

@app.post("/api/image/resize")
async def resize_image(
    file: UploadFile = File(...),
    width: int = Form(...),
    height: int = Form(...),
    maintain_aspect: bool = Form(True)
):
    """Resize image to specific pixel dimensions."""
    content = await file.read()
    im = Image.open(io.BytesIO(content))

    if maintain_aspect:
        im.thumbnail((width, height), Image.Resampling.LANCZOS)
    else:
        im = im.resize((width, height), Image.Resampling.LANCZOS)

    output = io.BytesIO()
    fmt = im.format or "PNG"
    im.save(output, format=fmt)
    output.seek(0)
    return StreamingResponse(
        output,
        media_type=f"image/{fmt.lower()}",
        headers={"Content-Disposition": f"attachment; filename=resized_{file.filename}"}
    )

@app.post("/api/image/filter")
async def filter_image(
    file: UploadFile = File(...),
    filter_type: str = Form("grayscale"),
    intensity: float = Form(1.0)
):
    """Apply rich image filters: grayscale, sepia, blur, sharpen, invert, contrast, brightness."""
    content = await file.read()
    im = Image.open(io.BytesIO(content))
    filter_type = filter_type.lower()

    if filter_type == "grayscale":
        im = ImageOps.grayscale(im)
    elif filter_type == "invert":
        if im.mode == "RGBA":
            r, g, b, a = im.split()
            rgb = Image.merge("RGB", (r, g, b))
            inv = ImageOps.invert(rgb)
            r2, g2, b2 = inv.split()
            im = Image.merge("RGBA", (r2, g2, b2, a))
        else:
            im = ImageOps.invert(im.convert("RGB"))
    elif filter_type == "sepia":
        gray = ImageOps.grayscale(im)
        im = ImageOps.colorize(gray, "#704214", "#ffecb3")
    elif filter_type == "blur":
        im = im.filter(ImageFilter.GaussianBlur(radius=intensity * 4))
    elif filter_type == "sharpen":
        im = im.filter(ImageFilter.UnsharpMask(radius=2, percent=int(intensity * 150)))
    elif filter_type == "brightness":
        enhancer = ImageEnhance.Brightness(im)
        im = enhancer.enhance(intensity)
    elif filter_type == "contrast":
        enhancer = ImageEnhance.Contrast(im)
        im = enhancer.enhance(intensity)
    elif filter_type == "saturation":
        enhancer = ImageEnhance.Color(im)
        im = enhancer.enhance(intensity)

    output = io.BytesIO()
    fmt = "PNG" if im.mode in ("RGBA", "P") else "JPEG"
    im.save(output, format=fmt)
    output.seek(0)
    return StreamingResponse(
        output,
        media_type=f"image/{fmt.lower()}",
        headers={"Content-Disposition": f"attachment; filename=filtered_{filter_type}.png"}
    )

@app.post("/api/image/metadata")
async def inspect_image_metadata(file: UploadFile = File(...)):
    """Inspect and extract EXIF metadata from image."""
    content = await file.read()
    im = Image.open(io.BytesIO(content))
    
    exif_data = {}
    exif = im.getexif()
    if exif:
        for tag_id, value in exif.items():
            tag_name = str(tag_id)
            exif_data[tag_name] = str(value)

    return {
        "filename": file.filename,
        "format": im.format,
        "mode": im.mode,
        "width": im.width,
        "height": im.height,
        "has_exif": len(exif_data) > 0,
        "exif": exif_data
    }

# ═══════════════════════════════════════════════════════════════════
# DOCUMENT & CONVERTER TOOLS
# ═══════════════════════════════════════════════════════════════════

@app.post("/api/doc/docx-to-text")
async def docx_to_text(file: UploadFile = File(...)):
    """Extract plain text and structured paragraphs from a Microsoft Word .docx file."""
    content = await file.read()
    doc = docx.Document(io.BytesIO(content))

    paragraphs = [p.text for p in doc.paragraphs if p.text.strip()]
    tables_data = []
    for t in doc.tables:
        rows = []
        for r in t.rows:
            rows.append([cell.text.strip() for cell in r.cells])
        tables_data.append(rows)

    full_text = "\n\n".join(paragraphs)
    return {
        "filename": file.filename,
        "paragraph_count": len(paragraphs),
        "table_count": len(tables_data),
        "word_count": len(full_text.split()),
        "character_count": len(full_text),
        "text": full_text,
        "tables": tables_data
    }

@app.post("/api/doc/docx-to-pdf")
async def docx_to_pdf(file: UploadFile = File(...)):
    """Convert .docx file content to a clean formatted PDF document."""
    content = await file.read()
    doc = docx.Document(io.BytesIO(content))

    pdf_buffer = io.BytesIO()
    pdf = SimpleDocTemplate(pdf_buffer, pagesize=letter, rightMargin=54, leftMargin=54, topMargin=54, bottomMargin=54)
    styles = getSampleStyleSheet()
    story = []

    body_style = ParagraphStyle(
        'NormalBody',
        parent=styles['Normal'],
        fontName='Helvetica',
        fontSize=10,
        leading=14,
        textColor='#1a1a1a',
        spaceAfter=8
    )
    h1_style = ParagraphStyle(
        'Heading1Custom',
        parent=styles['Heading1'],
        fontName='Helvetica-Bold',
        fontSize=18,
        leading=22,
        textColor='#16a34a',
        spaceAfter=12
    )

    for p in doc.paragraphs:
        txt = p.text.strip()
        if not txt:
            continue
        # Clean text for ReportLab XML parser
        safe_text = txt.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
        if p.style.name.startswith("Heading 1") or len(safe_text) < 40 and safe_text.isupper():
            story.append(Paragraph(safe_text, h1_style))
        else:
            story.append(Paragraph(safe_text, body_style))
        story.append(Spacer(1, 4))

    pdf.build(story)
    pdf_buffer.seek(0)
    return StreamingResponse(
        pdf_buffer,
        media_type="application/pdf",
        headers={"Content-Disposition": "attachment; filename=converted_document.pdf"}
    )

@app.post("/api/doc/csv-to-excel")
async def csv_to_excel(file: UploadFile = File(...)):
    """Convert CSV file to formatted Microsoft Excel (.xlsx) file with auto-width columns."""
    content = await file.read()
    text = content.decode('utf-8', errors='ignore')
    
    df = pd.read_csv(io.StringIO(text))
    output = io.BytesIO()
    with pd.ExcelWriter(output, engine='openpyxl') as writer:
        df.to_excel(writer, index=False, sheet_name='Sheet1')
        worksheet = writer.sheets['Sheet1']
        for col in worksheet.columns:
            max_len = max(len(str(cell.value or '')) for cell in col)
            col_letter = openpyxl.utils.get_column_letter(col[0].column)
            worksheet.column_dimensions[col_letter].width = min(max_len + 3, 50)
            
    output.seek(0)
    return StreamingResponse(
        output,
        media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
        headers={"Content-Disposition": "attachment; filename=converted_data.xlsx"}
    )

@app.post("/api/doc/excel-to-csv")
async def excel_to_csv(file: UploadFile = File(...)):
    """Convert Excel (.xlsx, .xls) file to clean CSV."""
    content = await file.read()
    df = pd.read_excel(io.BytesIO(content))
    output = io.StringIO()
    df.to_csv(output, index=False)
    csv_bytes = output.getvalue().encode('utf-8')
    return StreamingResponse(
        io.BytesIO(csv_bytes),
        media_type="text/csv",
        headers={"Content-Disposition": "attachment; filename=converted_data.csv"}
    )

@app.post("/api/doc/json-to-csv")
async def json_to_csv(data: str = Form(...)):
    """Convert JSON array of objects to CSV formatted table."""
    try:
        parsed = json.loads(data)
        if isinstance(parsed, dict):
            parsed = [parsed]
        if not isinstance(parsed, list):
            raise ValueError("Expected JSON array of objects.")
        
        df = pd.json_normalize(parsed)
        out = io.StringIO()
        df.to_csv(out, index=False)
        return {"csv": out.getvalue()}
    except Exception as e:
        raise HTTPException(status_code=400, detail=f"Invalid JSON data: {str(e)}")

# ═══════════════════════════════════════════════════════════════════
# UTILITY TOOLS (QR, Hash, Diff)
# ═══════════════════════════════════════════════════════════════════

@app.post("/api/tools/qr-generate")
async def generate_qr(
    data: str = Form(...),
    fill_color: str = Form("#16a34a"),
    back_color: str = Form("#ffffff"),
    size: int = Form(10),
    border: int = Form(2)
):
    """Generate high-resolution custom colored QR code."""
    qr = qrcode.QRCode(
        version=1,
        error_correction=qrcode.constants.ERROR_CORRECT_M,
        box_size=size,
        border=border
    )
    qr.add_data(data)
    qr.make(fit=True)

    img = qr.make_image(fill_color=fill_color, back_color=back_color)
    output = io.BytesIO()
    img.save(output, format="PNG")
    output.seek(0)
    return StreamingResponse(
        output,
        media_type="image/png",
        headers={"Content-Disposition": "attachment; filename=qrcode.png"}
    )

@app.post("/api/tools/hash")
async def calculate_hashes(
    text: Optional[str] = Form(None),
    file: Optional[UploadFile] = File(None)
):
    """Calculate MD5, SHA-1, SHA-256, SHA-512 cryptographic hashes of text or file."""
    if file:
        data = await file.read()
    elif text is not None:
        data = text.encode('utf-8')
    else:
        raise HTTPException(status_code=400, detail="Must provide text or file.")

    return {
        "md5": hashlib.md5(data).hexdigest(),
        "sha1": hashlib.sha1(data).hexdigest(),
        "sha256": hashlib.sha256(data).hexdigest(),
        "sha512": hashlib.sha512(data).hexdigest(),
        "byte_length": len(data)
    }

@app.post("/api/tools/diff")
async def compare_diff(
    original: str = Form(...),
    modified: str = Form(...)
):
    """Generate side-by-side or unified textual difference."""
    orig_lines = original.splitlines(keepends=True)
    mod_lines = modified.splitlines(keepends=True)
    diff = list(difflib.unified_diff(orig_lines, mod_lines, fromfile="original", tofile="modified"))
    
    return {
        "unified_diff": "".join(diff),
        "changes_count": len([l for l in diff if l.startswith('+') or l.startswith('-')])
    }

# ═══════════════════════════════════════════════════════════════════
# MULTI-INPUT BATCH PIPELINE TOOLS
# ═══════════════════════════════════════════════════════════════════

@app.post("/api/image/batch-process")
async def batch_process_images(
    files: List[UploadFile] = File(...),
    target_format: str = Form("webp"),
    quality: int = Form(80),
    max_width: Optional[int] = Form(None),
    filter_type: Optional[str] = Form("none"),
    watermark_text: Optional[str] = Form(None)
):
    """Perform multi-stage batch processing on multiple images (convert + compress + resize + filter + watermark)."""
    if not files:
        raise HTTPException(status_code=400, detail="At least one image file is required.")

    zip_buffer = io.BytesIO()
    total_orig_bytes = 0
    total_new_bytes = 0
    results_meta = []
    ext = target_format.lower()
    if ext == "jpeg":
        ext = "jpg"

    with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zip_file:
        for idx, file in enumerate(files):
            orig_data = await file.read()
            orig_len = len(orig_data)
            total_orig_bytes += orig_len

            try:
                im = Image.open(io.BytesIO(orig_data))
                orig_dim = (im.width, im.height)

                # 1. Resize constraint if requested
                if max_width and im.width > max_width:
                    ratio = max_width / float(im.width)
                    new_height = int(float(im.height) * float(ratio))
                    im = im.resize((max_width, new_height), Image.Resampling.LANCZOS)

                # 2. Apply photographic filter if requested
                if filter_type and filter_type.lower() != "none":
                    ft = filter_type.lower()
                    if ft == "grayscale":
                        im = ImageOps.grayscale(im)
                    elif ft == "sepia":
                        gray = ImageOps.grayscale(im)
                        im = ImageOps.colorize(gray, "#704214", "#ffecb3")
                    elif ft == "invert":
                        if im.mode == "RGBA":
                            r, g, b, a = im.split()
                            rgb = Image.merge("RGB", (r, g, b))
                            inv = ImageOps.invert(rgb)
                            r2, g2, b2 = inv.split()
                            im = Image.merge("RGBA", (r2, g2, b2, a))
                        else:
                            im = ImageOps.invert(im.convert("RGB"))
                    elif ft == "blur":
                        im = im.filter(ImageFilter.GaussianBlur(radius=3))
                    elif ft == "sharpen":
                        im = im.filter(ImageFilter.UnsharpMask(radius=2, percent=150))

                # 3. Apply Watermark text if requested
                if watermark_text:
                    if im.mode != "RGBA":
                        im = im.convert("RGBA")
                    overlay = Image.new("RGBA", im.size, (255, 255, 255, 0))
                    draw = ImageDraw.Draw(overlay)
                    font_size = max(18, int(im.width / 24))
                    try:
                        font = ImageFont.truetype("arial.ttf", font_size)
                    except Exception:
                        font = ImageFont.load_default()
                    bbox = draw.textbbox((0, 0), watermark_text, font=font)
                    text_w = bbox[2] - bbox[0]
                    text_h = bbox[3] - bbox[1]
                    margin = 20
                    pos = (im.width - text_w - margin, im.height - text_h - margin)
                    draw.rectangle([pos[0]-8, pos[1]-4, pos[0]+text_w+8, pos[1]+text_h+4], fill=(0, 0, 0, 100))
                    draw.text(pos, watermark_text, fill=(255, 255, 255, 200), font=font)
                    im = Image.alpha_composite(im, overlay)

                # 4. Convert and format handling
                out_stream = io.BytesIO()
                fmt = target_format.upper()
                if fmt in ("JPG", "JPEG", "BMP") and im.mode in ("RGBA", "LA", "P"):
                    bg = Image.new("RGB", im.size, (255, 255, 255))
                    bg.paste(im, mask=im.split()[-1] if im.mode == "RGBA" else None)
                    im = bg

                save_kwargs = {"format": "JPEG" if fmt == "JPG" else fmt}
                if fmt in ("JPEG", "JPG", "WEBP"):
                    save_kwargs["quality"] = quality
                    save_kwargs["optimize"] = True

                im.save(out_stream, **save_kwargs)
                out_bytes = out_stream.getvalue()
                new_len = len(out_bytes)
                total_new_bytes += new_len

                base_name = os.path.splitext(file.filename)[0]
                out_filename = f"{base_name}_brotools.{ext}"
                zip_file.writestr(out_filename, out_bytes)

                results_meta.append({
                    "filename": out_filename,
                    "original_size": orig_len,
                    "new_size": new_len,
                    "saved_percent": round((1.0 - (new_len / orig_len)) * 100, 1) if orig_len > 0 else 0,
                    "dimensions": f"{im.width}x{im.height}"
                })
            except Exception as e:
                continue

    # Add summary manifest into the zip
    manifest = {
        "processed_count": len(results_meta),
        "total_original_bytes": total_orig_bytes,
        "total_new_bytes": total_new_bytes,
        "overall_saved_percent": round((1.0 - (total_new_bytes / total_orig_bytes)) * 100, 1) if total_orig_bytes > 0 else 0,
        "files": results_meta
    }
    with zipfile.ZipFile(zip_buffer, "a", zipfile.ZIP_DEFLATED) as zip_file:
        zip_file.writestr("processing_manifest.json", json.dumps(manifest, indent=2))

    zip_buffer.seek(0)
    saved_pct = manifest["overall_saved_percent"]
    return StreamingResponse(
        zip_buffer,
        media_type="application/zip",
        headers={
            "Content-Disposition": "attachment; filename=brotools_batch_processed.zip",
            "X-Saved-Percent": str(saved_pct),
            "X-Processed-Count": str(len(results_meta))
        }
    )

@app.post("/api/pdf/pipeline")
async def pdf_pipeline(
    files: List[UploadFile] = File(...),
    rotate_angle: int = Form(0),
    watermark_text: Optional[str] = Form(None),
    password: Optional[str] = Form(None)
):
    """Unified PDF multi-action pipeline: merges multiple PDFs, applies rotation, watermarking, and password encryption in one pass."""
    if not files:
        raise HTTPException(status_code=400, detail="At least one PDF file is required.")

    merger = pypdf.PdfWriter()
    for f in files:
        content = await f.read()
        merger.append(io.BytesIO(content))

    merged_stream = io.BytesIO()
    merger.write(merged_stream)
    merger.close()
    merged_stream.seek(0)

    reader = pypdf.PdfReader(merged_stream)
    writer = pypdf.PdfWriter()

    # Optional watermark canvas
    wm_reader = None
    if watermark_text and watermark_text.strip():
        wm_buffer = io.BytesIO()
        first_page = reader.pages[0]
        w = float(first_page.mediabox.width)
        h = float(first_page.mediabox.height)
        can = canvas.Canvas(wm_buffer, pagesize=(w, h))
        can.saveState()
        can.setFont("Helvetica-Bold", 44)
        can.setFillColorRGB(0.5, 0.5, 0.5, alpha=0.25)
        can.translate(w / 2, h / 2)
        can.rotate(45)
        can.drawCentredString(0, 0, watermark_text.strip())
        can.restoreState()
        can.save()
        wm_buffer.seek(0)
        wm_reader = pypdf.PdfReader(wm_buffer)

    for page in reader.pages:
        if rotate_angle in (90, 180, 270):
            page.rotate(rotate_angle)
        if wm_reader:
            page.merge_page(wm_reader.pages[0])
        writer.add_page(page)

    if password and password.strip():
        writer.encrypt(password.strip())

    final_output = io.BytesIO()
    writer.write(final_output)
    final_output.seek(0)

    return StreamingResponse(
        final_output,
        media_type="application/pdf",
        headers={"Content-Disposition": "attachment; filename=brotools_pipeline_document.pdf"}
    )

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="127.0.0.1", port=8000)
