import io
import re

import pikepdf
from django.http import HttpResponse
from rest_framework import viewsets, status
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.parsers import MultiPartParser
from rest_framework.permissions import IsAuthenticated

from .models import PdfOperation
from .serializers import PdfOperationSerializer, PdfOperationCreateSerializer


# ─── helpers ──────────────────────────────────────────────────────────────────

def _pdf_response(data: bytes, filename: str, extra_headers: dict = None) -> HttpResponse:
    r = HttpResponse(data, content_type="application/pdf")
    r["Content-Disposition"] = f'attachment; filename="{filename}"'
    if extra_headers:
        for k, v in extra_headers.items():
            r[k] = v
        r["Access-Control-Expose-Headers"] = ", ".join(extra_headers.keys())
    return r


def _file_response(data: bytes, content_type: str, filename: str) -> HttpResponse:
    r = HttpResponse(data, content_type=content_type)
    r["Content-Disposition"] = f'attachment; filename="{filename}"'
    return r


def _require_file(request, key="file"):
    f = request.FILES.get(key)
    if not f:
        return None, Response({"error": f"No '{key}' file provided."}, status=400)
    return f, None


def _extract_text_from_pdf(pdf_bytes: bytes) -> list[str]:
    """Return list of strings, one per page, using pdfminer.six."""
    from pdfminer.high_level import extract_text_to_fp
    from pdfminer.layout import LAParams
    from io import BytesIO, StringIO

    pages = []
    # extract per-page by using page_numbers arg
    from pdfminer.high_level import extract_pages
    from pdfminer.layout import LTTextContainer

    for page_layout in extract_pages(BytesIO(pdf_bytes), laparams=LAParams()):
        page_text = []
        for element in page_layout:
            if isinstance(element, LTTextContainer):
                page_text.append(element.get_text())
        pages.append("".join(page_text))
    return pages


# ─── existing viewset ─────────────────────────────────────────────────────────

class PdfOperationViewSet(viewsets.ModelViewSet):
    queryset = PdfOperation.objects.all()

    def get_serializer_class(self):
        if self.action in ["create", "update", "partial_update"]:
            return PdfOperationCreateSerializer
        return PdfOperationSerializer

    def get_queryset(self):
        user = self.request.user
        if user.is_authenticated:
            return PdfOperation.objects.filter(created_by=user)
        return PdfOperation.objects.none()

    def perform_create(self, serializer):
        serializer.save(created_by=self.request.user)


# ─── compress ─────────────────────────────────────────────────────────────────

def _recompress_images(pdf: pikepdf.Pdf, jpeg_quality: int) -> None:
    from PIL import Image

    seen = set()
    for page in pdf.pages:
        try:
            images = page.images
        except Exception:
            continue
        for name in images:
            try:
                raw = images[name]
                obj_id = raw.objgen
                if obj_id in seen:
                    continue
                seen.add(obj_id)
                pdfimage = pikepdf.PdfImage(raw)
                pil_img = pdfimage.as_pil_image()
                if pil_img.width < 32 or pil_img.height < 32:
                    continue
                if pil_img.mode not in ("RGB", "L"):
                    pil_img = pil_img.convert("RGB")
                buf = io.BytesIO()
                pil_img.save(buf, format="JPEG", quality=jpeg_quality, optimize=True)
                jpeg_data = buf.getvalue()
                try:
                    original_size = len(raw.read_raw_bytes())
                except Exception:
                    original_size = len(jpeg_data) + 1
                if len(jpeg_data) >= original_size:
                    continue
                raw.write(jpeg_data, filter=pikepdf.Name.DCTDecode)
                raw["/ColorSpace"] = pikepdf.Name.DeviceGray if pil_img.mode == "L" else pikepdf.Name.DeviceRGB
                raw["/BitsPerComponent"] = 8
                try:
                    del raw["/DecodeParms"]
                except Exception:
                    pass
            except Exception:
                pass


class CompressPdfView(APIView):
    parser_classes = [MultiPartParser]
    permission_classes = [IsAuthenticated]

    def post(self, request):
        file_obj, err = _require_file(request)
        if err:
            return err
        level = request.data.get("level", "balanced")
        if level not in ("light", "balanced", "aggressive"):
            level = "balanced"
        cfg = {
            "light":      {"jpeg_quality": 85, "recompress_images": False},
            "balanced":   {"jpeg_quality": 72, "recompress_images": True},
            "aggressive": {"jpeg_quality": 50, "recompress_images": True},
        }[level]
        try:
            input_data = file_obj.read()
            with pikepdf.open(io.BytesIO(input_data)) as pdf:
                with pdf.open_metadata(set_pikepdf_as_editor=False) as meta:
                    meta.clear()
                if "/Info" in pdf.trailer:
                    info = pdf.trailer["/Info"]
                    for key in list(info.keys()):
                        try:
                            del info[key]
                        except Exception:
                            pass
                if cfg["recompress_images"]:
                    _recompress_images(pdf, cfg["jpeg_quality"])
                out_buf = io.BytesIO()
                pdf.save(out_buf, compress_streams=True, recompress_flate=True,
                         object_stream_mode=pikepdf.ObjectStreamMode.generate)
                compressed = out_buf.getvalue()
            base = file_obj.name.rsplit(".", 1)[0]
            return _pdf_response(compressed, f"{base}_compressed.pdf", {
                "X-Original-Size": str(len(input_data)),
                "X-Compressed-Size": str(len(compressed)),
            })
        except pikepdf.PasswordError:
            return Response({"error": "PDF is password-protected."}, status=422)
        except Exception as e:
            return Response({"error": str(e)}, status=500)


# ─── protect ──────────────────────────────────────────────────────────────────

class ProtectPdfView(APIView):
    parser_classes = [MultiPartParser]
    permission_classes = [IsAuthenticated]

    def post(self, request):
        file_obj, err = _require_file(request)
        if err:
            return err
        password = request.data.get("password", "")
        if not password:
            return Response({"error": "Password is required."}, status=400)
        allow_print  = request.data.get("allow_print",  "true") == "true"
        allow_copy   = request.data.get("allow_copy",   "true") == "true"
        allow_modify = request.data.get("allow_modify", "false") == "true"
        try:
            pdf_bytes = file_obj.read()
            with pikepdf.open(io.BytesIO(pdf_bytes)) as pdf:
                perms = pikepdf.Permissions(
                    print_lowres=allow_print,
                    print_highres=allow_print,
                    extract=allow_copy,
                    modify_annotation=allow_modify,
                    modify_form=allow_modify,
                    modify_other=allow_modify,
                    modify_assembly=False,
                    accessibility=True,
                )
                enc = pikepdf.Encryption(user=password, owner=password + "_owner", allow=perms)
                out_buf = io.BytesIO()
                pdf.save(out_buf, encryption=enc)
                out_bytes = out_buf.getvalue()
            base = file_obj.name.rsplit(".", 1)[0]
            return _pdf_response(out_bytes, f"{base}_protected.pdf")
        except Exception as e:
            return Response({"error": str(e)}, status=500)


# ─── unlock ───────────────────────────────────────────────────────────────────

class UnlockPdfView(APIView):
    parser_classes = [MultiPartParser]
    permission_classes = [IsAuthenticated]

    def post(self, request):
        file_obj, err = _require_file(request)
        if err:
            return err
        password = request.data.get("password", "")
        try:
            pdf_bytes = file_obj.read()
            with pikepdf.open(io.BytesIO(pdf_bytes), password=password) as pdf:
                out_buf = io.BytesIO()
                pdf.save(out_buf, compress_streams=True,
                         object_stream_mode=pikepdf.ObjectStreamMode.generate)
                out_bytes = out_buf.getvalue()
            base = file_obj.name.rsplit(".", 1)[0]
            return _pdf_response(out_bytes, f"{base}_unlocked.pdf")
        except pikepdf.PasswordError:
            return Response({"error": "Incorrect password."}, status=422)
        except Exception as e:
            return Response({"error": str(e)}, status=500)


# ─── repair ───────────────────────────────────────────────────────────────────

class RepairPdfView(APIView):
    parser_classes = [MultiPartParser]
    permission_classes = [IsAuthenticated]

    def post(self, request):
        file_obj, err = _require_file(request)
        if err:
            return err
        try:
            pdf_bytes = file_obj.read()
            with pikepdf.open(io.BytesIO(pdf_bytes), suppress_warnings=True) as pdf:
                out_buf = io.BytesIO()
                pdf.save(out_buf, compress_streams=True,
                         recompress_flate=True,
                         object_stream_mode=pikepdf.ObjectStreamMode.generate)
                out_bytes = out_buf.getvalue()
            base = file_obj.name.rsplit(".", 1)[0]
            return _pdf_response(out_bytes, f"{base}_repaired.pdf", {
                "X-Original-Size": str(len(pdf_bytes)),
                "X-Repaired-Size":  str(len(out_bytes)),
            })
        except Exception as e:
            return Response({"error": str(e)}, status=500)


# ─── watermark ────────────────────────────────────────────────────────────────

class WatermarkPdfView(APIView):
    parser_classes = [MultiPartParser]
    permission_classes = [IsAuthenticated]

    def post(self, request):
        file_obj, err = _require_file(request)
        if err:
            return err
        text       = request.data.get("text", "CONFIDENTIAL")
        font_size  = int(request.data.get("font_size", "48"))
        opacity    = float(request.data.get("opacity", "0.3"))
        rotation   = float(request.data.get("rotation", "45"))
        color_hex  = request.data.get("color", "#000000").lstrip("#")
        r_val = int(color_hex[0:2], 16) / 255
        g_val = int(color_hex[2:4], 16) / 255
        b_val = int(color_hex[4:6], 16) / 255
        try:
            from reportlab.pdfgen import canvas as rl_canvas
            from reportlab.lib.colors import Color

            pdf_bytes = file_obj.read()
            with pikepdf.open(io.BytesIO(pdf_bytes)) as pdf:
                for page in pdf.pages:
                    mediabox = page.mediabox
                    pw = float(mediabox[2])
                    ph = float(mediabox[3])

                    wm_buf = io.BytesIO()
                    c = rl_canvas.Canvas(wm_buf, pagesize=(pw, ph))
                    c.saveState()
                    c.setFillColor(Color(r_val, g_val, b_val, alpha=opacity))
                    c.setFont("Helvetica", font_size)
                    c.translate(pw / 2, ph / 2)
                    c.rotate(rotation)
                    c.drawCentredString(0, 0, text)
                    c.restoreState()
                    c.save()
                    wm_buf.seek(0)

                    wm_pdf = pikepdf.open(wm_buf)
                    page.add_overlay(wm_pdf.pages[0], pikepdf.Rectangle(0, 0, pw, ph))

                out_buf = io.BytesIO()
                pdf.save(out_buf)
                out_bytes = out_buf.getvalue()

            base = file_obj.name.rsplit(".", 1)[0]
            return _pdf_response(out_bytes, f"{base}_watermarked.pdf")
        except Exception as e:
            return Response({"error": str(e)}, status=500)


# ─── extract text ─────────────────────────────────────────────────────────────

class ExtractTextView(APIView):
    parser_classes = [MultiPartParser]
    permission_classes = [IsAuthenticated]

    def post(self, request):
        file_obj, err = _require_file(request)
        if err:
            return err
        output_format = request.data.get("format", "txt")  # txt or pdf
        try:
            pdf_bytes = file_obj.read()
            pages = _extract_text_from_pdf(pdf_bytes)
            full_text = "\n\n--- Page Break ---\n\n".join(
                f"=== Page {i+1} ===\n{p.strip()}" for i, p in enumerate(pages)
            )
            base = file_obj.name.rsplit(".", 1)[0]
            if output_format == "txt":
                return _file_response(full_text.encode("utf-8"), "text/plain", f"{base}_extracted.txt")
            else:
                # Create simple searchable PDF with reportlab
                from reportlab.pdfgen import canvas as rl_canvas
                from reportlab.lib.pagesizes import A4
                from reportlab.lib.units import cm

                buf = io.BytesIO()
                c = rl_canvas.Canvas(buf, pagesize=A4)
                w, h = A4
                margin = 2 * cm
                y = h - margin
                c.setFont("Helvetica", 10)

                for i, page_text in enumerate(pages):
                    if i > 0:
                        c.showPage()
                        y = h - margin
                        c.setFont("Helvetica", 10)
                    c.setFont("Helvetica-Bold", 12)
                    c.drawString(margin, y, f"Page {i+1}")
                    y -= 20
                    c.setFont("Helvetica", 10)
                    for line in page_text.splitlines():
                        if y < margin:
                            c.showPage()
                            y = h - margin
                            c.setFont("Helvetica", 10)
                        c.drawString(margin, y, line[:100])
                        y -= 14
                c.save()
                buf.seek(0)
                return _pdf_response(buf.getvalue(), f"{base}_extracted.pdf")
        except Exception as e:
            return Response({"error": str(e)}, status=500)


# ─── pdf to word ──────────────────────────────────────────────────────────────

class PdfToWordView(APIView):
    parser_classes = [MultiPartParser]
    permission_classes = [IsAuthenticated]

    def post(self, request):
        file_obj, err = _require_file(request)
        if err:
            return err
        try:
            from docx import Document
            from docx.shared import Pt, Inches

            pdf_bytes = file_obj.read()
            pages = _extract_text_from_pdf(pdf_bytes)

            doc = Document()
            doc.core_properties.title = file_obj.name.rsplit(".", 1)[0]

            for i, page_text in enumerate(pages):
                if i > 0:
                    doc.add_page_break()
                h = doc.add_heading(f"Page {i+1}", level=2)
                h.style.font.size = Pt(10)
                for para in page_text.split("\n\n"):
                    para = para.strip()
                    if para:
                        doc.add_paragraph(para)

            buf = io.BytesIO()
            doc.save(buf)
            buf.seek(0)
            base = file_obj.name.rsplit(".", 1)[0]
            return _file_response(
                buf.getvalue(),
                "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
                f"{base}.docx",
            )
        except Exception as e:
            return Response({"error": str(e)}, status=500)


# ─── pdf to excel ─────────────────────────────────────────────────────────────

class PdfToExcelView(APIView):
    parser_classes = [MultiPartParser]
    permission_classes = [IsAuthenticated]

    def post(self, request):
        file_obj, err = _require_file(request)
        if err:
            return err
        try:
            import openpyxl
            from openpyxl.styles import Font, PatternFill, Alignment

            pdf_bytes = file_obj.read()
            pages = _extract_text_from_pdf(pdf_bytes)

            wb = openpyxl.Workbook()
            wb.remove(wb.active)  # remove default sheet

            for i, page_text in enumerate(pages):
                ws = wb.create_sheet(title=f"Page {i+1}")
                # header
                ws["A1"] = f"Page {i+1}"
                ws["A1"].font = Font(bold=True, size=12)
                ws["A1"].fill = PatternFill("solid", fgColor="E8F0FE")
                row = 2
                for line in page_text.splitlines():
                    line = line.strip()
                    if not line:
                        row += 1
                        continue
                    # Try to split tab/multi-space separated columns
                    cols = re.split(r"\s{2,}|\t", line)
                    for col_idx, cell_val in enumerate(cols, start=1):
                        ws.cell(row=row, column=col_idx, value=cell_val.strip())
                    row += 1
                ws.column_dimensions["A"].width = 50

            buf = io.BytesIO()
            wb.save(buf)
            buf.seek(0)
            base = file_obj.name.rsplit(".", 1)[0]
            return _file_response(
                buf.getvalue(),
                "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
                f"{base}.xlsx",
            )
        except Exception as e:
            return Response({"error": str(e)}, status=500)


# ─── pdf to powerpoint ────────────────────────────────────────────────────────

class PdfToPowerpointView(APIView):
    parser_classes = [MultiPartParser]
    permission_classes = [IsAuthenticated]

    def post(self, request):
        file_obj, err = _require_file(request)
        if err:
            return err
        try:
            from pptx import Presentation
            from pptx.util import Inches, Pt
            from pptx.dml.color import RGBColor

            pdf_bytes = file_obj.read()
            pages = _extract_text_from_pdf(pdf_bytes)

            prs = Presentation()
            prs.slide_width  = Inches(13.33)
            prs.slide_height = Inches(7.5)
            blank_layout = prs.slide_layouts[6]

            for i, page_text in enumerate(pages):
                slide = prs.slides.add_slide(blank_layout)
                # Title box
                title_box = slide.shapes.add_textbox(Inches(0.5), Inches(0.2), Inches(12.3), Inches(0.6))
                tf = title_box.text_frame
                tf.text = f"Page {i+1}"
                tf.paragraphs[0].runs[0].font.size = Pt(14)
                tf.paragraphs[0].runs[0].font.bold = True
                tf.paragraphs[0].runs[0].font.color.rgb = RGBColor(0x44, 0x44, 0x44)
                # Content box
                content_box = slide.shapes.add_textbox(Inches(0.5), Inches(0.9), Inches(12.3), Inches(6.4))
                tf2 = content_box.text_frame
                tf2.word_wrap = True
                first = True
                for para in page_text.split("\n\n"):
                    para = para.strip()
                    if not para:
                        continue
                    p = tf2.paragraphs[0] if first else tf2.add_paragraph()
                    first = False
                    p.text = para
                    p.runs[0].font.size = Pt(11)

            buf = io.BytesIO()
            prs.save(buf)
            buf.seek(0)
            base = file_obj.name.rsplit(".", 1)[0]
            return _file_response(
                buf.getvalue(),
                "application/vnd.openxmlformats-officedocument.presentationml.presentation",
                f"{base}.pptx",
            )
        except Exception as e:
            return Response({"error": str(e)}, status=500)


# ─── word to pdf ──────────────────────────────────────────────────────────────

class WordToPdfView(APIView):
    parser_classes = [MultiPartParser]
    permission_classes = [IsAuthenticated]

    def post(self, request):
        file_obj, err = _require_file(request)
        if err:
            return err
        try:
            from docx import Document
            from reportlab.pdfgen import canvas as rl_canvas
            from reportlab.lib.pagesizes import A4
            from reportlab.lib.units import cm
            from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak
            from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
            from reportlab.lib.enums import TA_LEFT

            doc = Document(io.BytesIO(file_obj.read()))
            styles = getSampleStyleSheet()
            normal = ParagraphStyle("Normal2", parent=styles["Normal"], fontSize=11, leading=16)
            heading1 = ParagraphStyle("H1", parent=styles["Heading1"], fontSize=16, leading=20)
            heading2 = ParagraphStyle("H2", parent=styles["Heading2"], fontSize=13, leading=18)

            story = []
            for para in doc.paragraphs:
                text = para.text.strip()
                if not text:
                    story.append(Spacer(1, 8))
                    continue
                style_name = para.style.name or ""
                if "Heading 1" in style_name:
                    story.append(Paragraph(text, heading1))
                elif "Heading 2" in style_name:
                    story.append(Paragraph(text, heading2))
                else:
                    story.append(Paragraph(text, normal))
            if not story:
                story.append(Paragraph("(empty document)", normal))

            buf = io.BytesIO()
            pdf_doc = SimpleDocTemplate(buf, pagesize=A4,
                                        leftMargin=2*cm, rightMargin=2*cm,
                                        topMargin=2*cm, bottomMargin=2*cm)
            pdf_doc.build(story)
            buf.seek(0)
            base = file_obj.name.rsplit(".", 1)[0]
            return _pdf_response(buf.getvalue(), f"{base}.pdf")
        except Exception as e:
            return Response({"error": str(e)}, status=500)


# ─── excel to pdf ─────────────────────────────────────────────────────────────

class ExcelToPdfView(APIView):
    parser_classes = [MultiPartParser]
    permission_classes = [IsAuthenticated]

    def post(self, request):
        file_obj, err = _require_file(request)
        if err:
            return err
        try:
            import openpyxl
            from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, PageBreak
            from reportlab.lib.pagesizes import A4, landscape
            from reportlab.lib.styles import getSampleStyleSheet
            from reportlab.lib import colors
            from reportlab.lib.units import cm

            wb = openpyxl.load_workbook(io.BytesIO(file_obj.read()), data_only=True)
            styles = getSampleStyleSheet()
            story = []
            first = True

            for sheet_name in wb.sheetnames:
                ws = wb[sheet_name]
                if not first:
                    story.append(PageBreak())
                first = False
                story.append(Paragraph(f"Sheet: {sheet_name}", styles["Heading2"]))
                story.append(Spacer(1, 8))

                rows = list(ws.iter_rows(values_only=True))
                if not rows:
                    story.append(Paragraph("(empty sheet)", styles["Normal"]))
                    continue

                # Limit columns for readability
                max_cols = min(len(rows[0]) if rows else 0, 10)
                table_data = []
                for row in rows[:200]:
                    table_data.append([str(c) if c is not None else "" for c in row[:max_cols]])

                if table_data:
                    col_width = (A4[0] - 4*cm) / max(max_cols, 1)
                    t = Table(table_data, colWidths=[col_width] * max_cols)
                    t.setStyle(TableStyle([
                        ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#4A86C8")),
                        ("TEXTCOLOR",  (0, 0), (-1, 0), colors.white),
                        ("FONTNAME",   (0, 0), (-1, 0), "Helvetica-Bold"),
                        ("FONTSIZE",   (0, 0), (-1, -1), 8),
                        ("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.HexColor("#F5F5F5")]),
                        ("GRID", (0, 0), (-1, -1), 0.5, colors.HexColor("#CCCCCC")),
                        ("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
                        ("PADDING", (0, 0), (-1, -1), 4),
                    ]))
                    story.append(t)

            buf = io.BytesIO()
            doc = SimpleDocTemplate(buf, pagesize=A4,
                                    leftMargin=2*cm, rightMargin=2*cm,
                                    topMargin=2*cm, bottomMargin=2*cm)
            doc.build(story)
            buf.seek(0)
            base = file_obj.name.rsplit(".", 1)[0]
            return _pdf_response(buf.getvalue(), f"{base}.pdf")
        except Exception as e:
            return Response({"error": str(e)}, status=500)


# ─── powerpoint to pdf ────────────────────────────────────────────────────────

class PowerpointToPdfView(APIView):
    parser_classes = [MultiPartParser]
    permission_classes = [IsAuthenticated]

    def post(self, request):
        file_obj, err = _require_file(request)
        if err:
            return err
        try:
            from pptx import Presentation
            from pptx.util import Inches
            from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak
            from reportlab.lib.pagesizes import landscape, A4
            from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
            from reportlab.lib.units import cm

            prs = Presentation(io.BytesIO(file_obj.read()))
            styles = getSampleStyleSheet()
            title_style = ParagraphStyle("SlideTitle", parent=styles["Heading1"], fontSize=18, leading=22)
            body_style  = ParagraphStyle("SlideBody",  parent=styles["Normal"],   fontSize=11, leading=16)

            story = []
            for idx, slide in enumerate(prs.slides):
                if idx > 0:
                    story.append(PageBreak())
                story.append(Paragraph(f"Slide {idx+1}", styles["Heading3"]))
                for shape in slide.shapes:
                    if not shape.has_text_frame:
                        continue
                    for para in shape.text_frame.paragraphs:
                        text = para.text.strip()
                        if not text:
                            continue
                        level = para.level
                        if level == 0 and idx == 0 or shape.shape_type == 13:  # title placeholder
                            story.append(Paragraph(text, title_style))
                        else:
                            indent = "  " * level
                            story.append(Paragraph(f"{indent}• {text}" if level > 0 else text, body_style))
                story.append(Spacer(1, 12))

            if not story:
                story.append(Paragraph("(empty presentation)", styles["Normal"]))

            buf = io.BytesIO()
            doc = SimpleDocTemplate(buf, pagesize=A4,
                                    leftMargin=2*cm, rightMargin=2*cm,
                                    topMargin=2*cm, bottomMargin=2*cm)
            doc.build(story)
            buf.seek(0)
            base = file_obj.name.rsplit(".", 1)[0]
            return _pdf_response(buf.getvalue(), f"{base}.pdf")
        except Exception as e:
            return Response({"error": str(e)}, status=500)


# ─── images to pdf ────────────────────────────────────────────────────────────

class ImagesToPdfView(APIView):
    parser_classes = [MultiPartParser]
    permission_classes = [IsAuthenticated]

    def post(self, request):
        from PIL import Image as PILImage
        from reportlab.platypus import SimpleDocTemplate, Image as RLImage
        from reportlab.lib.pagesizes import A4, letter, LEGAL
        from reportlab.lib.units import cm

        files = request.FILES.getlist("files")
        if not files:
            return Response({"error": "No image files provided."}, status=400)

        page_size_name = request.data.get("page_size", "A4").upper()
        orientation    = request.data.get("orientation", "Portrait")
        margin_name    = request.data.get("margin", "Normal")

        size_map = {"A4": A4, "LETTER": letter, "LEGAL": LEGAL}
        pw, ph = size_map.get(page_size_name, A4)
        if orientation.lower() == "landscape":
            pw, ph = ph, pw

        margin_map = {"None": 0, "Small": 1*cm, "Normal": 2*cm, "Large": 3*cm}
        margin = margin_map.get(margin_name, 2*cm)

        try:
            buf = io.BytesIO()
            doc = SimpleDocTemplate(buf, pagesize=(pw, ph),
                                    leftMargin=margin, rightMargin=margin,
                                    topMargin=margin, bottomMargin=margin)
            avail_w = pw - 2 * margin
            avail_h = ph - 2 * margin
            story = []

            for i, f in enumerate(files):
                img_data = f.read()
                img_buf = io.BytesIO(img_data)
                with PILImage.open(img_buf) as pil:
                    iw, ih = pil.size
                # Scale to fit
                scale = min(avail_w / iw, avail_h / ih)
                draw_w, draw_h = iw * scale, ih * scale
                img_buf.seek(0)
                if i > 0:
                    from reportlab.platypus import PageBreak
                    story.append(PageBreak())
                story.append(RLImage(img_buf, width=draw_w, height=draw_h))

            doc.build(story)
            buf.seek(0)
            return _pdf_response(buf.getvalue(), "images.pdf")
        except Exception as e:
            return Response({"error": str(e)}, status=500)


# ─── text to pdf ──────────────────────────────────────────────────────────────

class TextToPdfView(APIView):
    parser_classes = [MultiPartParser]
    permission_classes = [IsAuthenticated]

    def post(self, request):
        text    = request.data.get("text", "")
        font_sz = int(request.data.get("font_size", "12"))
        font_name = request.data.get("font", "Helvetica")
        file_obj = request.FILES.get("file")

        if file_obj:
            text = file_obj.read().decode("utf-8", errors="replace")

        if not text.strip():
            return Response({"error": "No text provided."}, status=400)

        try:
            from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
            from reportlab.lib.pagesizes import A4
            from reportlab.lib.styles import ParagraphStyle
            from reportlab.lib.units import cm

            style = ParagraphStyle("main", fontName=font_name, fontSize=font_sz, leading=font_sz * 1.4)
            story = []
            for para in text.split("\n\n"):
                para = para.strip()
                if not para:
                    story.append(Spacer(1, 8))
                    continue
                for line in para.split("\n"):
                    story.append(Paragraph(line or " ", style))
                story.append(Spacer(1, 6))

            buf = io.BytesIO()
            doc = SimpleDocTemplate(buf, pagesize=A4,
                                    leftMargin=2*cm, rightMargin=2*cm,
                                    topMargin=2*cm, bottomMargin=2*cm)
            doc.build(story)
            buf.seek(0)
            base = file_obj.name.rsplit(".", 1)[0] if file_obj else "document"
            return _pdf_response(buf.getvalue(), f"{base}.pdf")
        except Exception as e:
            return Response({"error": str(e)}, status=500)


# ─── html to pdf ──────────────────────────────────────────────────────────────

class HtmlToPdfView(APIView):
    parser_classes = [MultiPartParser]
    permission_classes = [IsAuthenticated]

    def post(self, request):
        html_text  = request.data.get("html", "")
        url_source = request.data.get("url", "")
        file_obj   = request.FILES.get("file")

        if file_obj:
            html_text = file_obj.read().decode("utf-8", errors="replace")

        if not html_text.strip() and not url_source:
            return Response({"error": "No HTML content provided."}, status=400)

        try:
            from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
            from reportlab.lib.pagesizes import A4
            from reportlab.lib.styles import getSampleStyleSheet
            from reportlab.lib.units import cm
            import html as html_module
            import re as _re

            # Strip HTML tags for basic text extraction
            if not html_text and url_source:
                html_text = f"<p>URL: {url_source}</p><p>(URL conversion requires server-side fetch)</p>"

            clean = _re.sub(r"<[^>]+>", " ", html_text)
            clean = html_module.unescape(clean)
            clean = _re.sub(r"\s+", " ", clean).strip()

            styles = getSampleStyleSheet()
            story = [Paragraph(clean[:50000], styles["Normal"])] if clean else [Paragraph("(empty)", styles["Normal"])]

            buf = io.BytesIO()
            doc = SimpleDocTemplate(buf, pagesize=A4,
                                    leftMargin=2*cm, rightMargin=2*cm,
                                    topMargin=2*cm, bottomMargin=2*cm)
            doc.build(story)
            buf.seek(0)
            base = file_obj.name.rsplit(".", 1)[0] if file_obj else "document"
            return _pdf_response(buf.getvalue(), f"{base}.pdf")
        except Exception as e:
            return Response({"error": str(e)}, status=500)


# ─── redact pdf ───────────────────────────────────────────────────────────────

class RedactPdfView(APIView):
    parser_classes = [MultiPartParser]
    permission_classes = [IsAuthenticated]

    def post(self, request):
        file_obj, err = _require_file(request)
        if err:
            return err

        terms_raw = request.data.get("terms", "")
        terms = [t.strip() for t in terms_raw.split(",") if t.strip()]
        if not terms:
            return Response({"error": "No redaction terms provided."}, status=400)

        try:
            from pdfminer.high_level import extract_pages
            from pdfminer.layout import LTTextContainer, LTChar, LTTextLine
            from reportlab.pdfgen import canvas as rl_canvas
            from reportlab.lib.colors import black

            pdf_bytes = file_obj.read()

            # ── 1. find character bounding boxes for each term ──
            redaction_boxes = {}  # {page_num: [(x0, y0, x1, y1), ...]}

            for page_num, page_layout in enumerate(extract_pages(io.BytesIO(pdf_bytes))):
                boxes = []
                for element in page_layout:
                    if not isinstance(element, LTTextContainer):
                        continue
                    for text_line in element:
                        if not isinstance(text_line, LTTextLine):
                            continue
                        line_chars = [
                            {"char": ch.get_text(), "bbox": ch.bbox}
                            for ch in text_line
                            if isinstance(ch, LTChar)
                        ]
                        if not line_chars:
                            continue
                        line_text = "".join(c["char"] for c in line_chars)
                        for term in terms:
                            start = 0
                            tl = term.lower()
                            while True:
                                idx = line_text.lower().find(tl, start)
                                if idx == -1:
                                    break
                                matched = line_chars[idx: idx + len(term)]
                                if matched:
                                    x0 = min(c["bbox"][0] for c in matched) - 1
                                    y0 = min(c["bbox"][1] for c in matched) - 2
                                    x1 = max(c["bbox"][2] for c in matched) + 1
                                    y1 = max(c["bbox"][3] for c in matched) + 2
                                    boxes.append((x0, y0, x1, y1))
                                start = idx + 1
                if boxes:
                    redaction_boxes[page_num] = boxes

            # ── 2. draw black boxes over matches ──
            with pikepdf.open(io.BytesIO(pdf_bytes)) as pdf:
                for page_num, page in enumerate(pdf.pages):
                    boxes = redaction_boxes.get(page_num)
                    if not boxes:
                        continue
                    mediabox = page.mediabox
                    pw = float(mediabox[2])
                    ph = float(mediabox[3])

                    overlay_buf = io.BytesIO()
                    c = rl_canvas.Canvas(overlay_buf, pagesize=(pw, ph))
                    c.setFillColor(black)
                    for (x0, y0, x1, y1) in boxes:
                        c.rect(x0, y0, x1 - x0, y1 - y0, fill=1, stroke=0)
                    c.save()
                    overlay_buf.seek(0)

                    overlay_pdf = pikepdf.open(overlay_buf)
                    page.add_overlay(overlay_pdf.pages[0], pikepdf.Rectangle(0, 0, pw, ph))

                out_buf = io.BytesIO()
                pdf.save(out_buf)
                out_bytes = out_buf.getvalue()

            base = file_obj.name.rsplit(".", 1)[0]
            return _pdf_response(out_bytes, f"{base}_redacted.pdf")
        except Exception as e:
            return Response({"error": str(e)}, status=500)
