"""Attachments manager: upload (text/HTML/PDF/image/file), HTML->PDF conversion,
list, download, preview, delete, Clear All. Uploads are validated, hashed and scanned."""
import asyncio
import base64
import hashlib
import re
from typing import Optional

from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile
from fastapi.responses import FileResponse
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession

from app.api.deps import get_current_user, get_db, require_csrf
from app.core.security import log_audit
from app.models import Attachment, EmailTemplate, User
from app.services import attachments_service as att_svc
from app.services.message_builder import html_to_pdf, sanitize_preview, validate_html

router = APIRouter(prefix="/attachments", tags=["attachments"])

MAX_UPLOAD_BYTES = 10 * 1024 * 1024  # 10 MB per file

ALLOWED_EXTENSIONS = {
    "text": {".txt", ".text"},
    "html": {".html", ".htm"},
    "pdf": {".pdf"},
    "image": {".png", ".jpg", ".jpeg", ".gif", ".webp"},
    "file": {".txt", ".pdf", ".png", ".jpg", ".jpeg", ".gif", ".webp",
             ".html", ".htm", ".csv", ".zip"},
}

MIME_BY_EXT = {
    ".txt": "text/plain", ".text": "text/plain", ".html": "text/html", ".htm": "text/html",
    ".pdf": "application/pdf", ".png": "image/png", ".jpg": "image/jpeg",
    ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp",
    ".csv": "text/csv", ".zip": "application/zip",
}


def _ext(name: str) -> str:
    m = re.search(r"(\.[A-Za-z0-9]+)$", name or "")
    return m.group(1).lower() if m else ""


def _kind_for(ext: str) -> Optional[str]:
    for kind, exts in ALLOWED_EXTENSIONS.items():
        if ext in exts:
            return kind
    return None


def _serialize(att: Attachment) -> dict:
    return {
        "id": att.id,
        "name": att.name,
        "kind": att.kind,
        "filename": att.filename,
        "mime_type": att.mime_type,
        "size_bytes": att.size_bytes,
        "file_sha256": att.file_sha256,
        "description": att.description,
        "is_html_attachment": bool(att.is_html_attachment),
        "created_at": att.created_at.isoformat() if att.created_at else None,
    }


async def _get_own(db: AsyncSession, user: User, att_id: int) -> Attachment:
    att = await db.get(Attachment, att_id)
    if not att or att.user_id != user.id:
        raise HTTPException(status_code=404, detail="Attachment not found")
    return att


@router.get("/list")
async def list_attachments(kind: str = Query("", description="filter by kind"),
                           search: str = Query(""),
                           page: int = Query(1, ge=1),
                           page_size: int = Query(50, ge=1, le=200),
                           db: AsyncSession = Depends(get_db),
                           user: User = Depends(get_current_user)):
    q = select(Attachment).where(Attachment.user_id == user.id)
    if kind:
        q = q.where(Attachment.kind == kind)
    if search:
        q = q.where(Attachment.name.ilike(f"%{search}%"))
    total = (await db.execute(
        select(func.count()).select_from(q.subquery()))).scalar() or 0
    rows = (await db.execute(
        q.order_by(Attachment.id.desc()).offset((page - 1) * page_size).limit(page_size)
    )).scalars().all()
    return {"attachments": [_serialize(a) for a in rows],
            "total": total, "page": page, "page_size": page_size}


@router.post("/upload")
async def upload_attachment(file: UploadFile = File(...),
                            name: str = Query(""),
                            description: str = Query(""),
                            db: AsyncSession = Depends(get_db),
                            user: User = Depends(get_current_user),
                            _: None = Depends(require_csrf)):
    """Upload an attachment. Kind inferred from extension; stored with random prefix path.
    HTML files are stored as editable HTML attachments (variables substituted per recipient)."""
    content = await file.read()
    if len(content) > MAX_UPLOAD_BYTES:
        raise HTTPException(status_code=413, detail="File exceeds 10MB limit")
    if not content:
        raise HTTPException(status_code=400, detail="Empty file")

    orig = file.filename or "attachment.bin"
    ext = _ext(orig)
    kind = _kind_for(ext)
    if kind is None:
        raise HTTPException(status_code=400,
                            detail=f"Unsupported file type: {ext or 'unknown'}. Allowed: "
                                   f"txt, html, pdf, png, jpg, gif, webp, csv, zip")

    # security: scan buffer for executable magic before persisting
    verdict, reason = att_svc.scan_for_malware(content, orig)
    if not verdict:
        raise HTTPException(status_code=400, detail=f"Rejected by malware scan: {reason}")

    is_html = kind == "html"
    html_body = ""
    if is_html:
        try:
            html_body = content.decode("utf-8")
        except UnicodeDecodeError:
            raise HTTPException(status_code=400, detail="HTML file is not valid UTF-8")
        problems = validate_html(html_body)
        # HTML attachments legitimately contain %Variables% (they are
        # substituted per recipient at send time), so filter that advisory
        # out while keeping every structural security check (script/iframe/
        # form tags, img without src, empty document, ...).
        problems = [p for p in problems if not p.startswith("Unsubstituted variables")]
        if problems:  # validate_html returns a list of problem strings
            raise HTTPException(status_code=400,
                                detail=f"HTML rejected: {'; '.join(problems)}")
        # HTML attachments are stored inline (used with variable substitution at send time)
        att = Attachment(
            user_id=user.id,
            name=name or re.sub(r"\.[^.]+$", "", orig) or "html-attachment",
            filename=orig,
            stored_path="",
            mime_type="text/html",
            size_bytes=len(content),
            kind="html",
            description=description or "",
            is_html_attachment=True,
            html_attachment_body=html_body,
            file_sha256=hashlib.sha256(content).hexdigest(),
        )
    else:
        stored = att_svc.save_upload(orig, content)
        att = Attachment(
            user_id=user.id,
            name=name or re.sub(r"\.[^.]+$", "", orig) or "attachment",
            filename=orig,
            stored_path=stored["stored_path"],
            mime_type=MIME_BY_EXT.get(ext, "application/octet-stream"),
            size_bytes=len(content),
            kind=kind,
            description=description or "",
            file_sha256=hashlib.sha256(content).hexdigest(),
        )
    db.add(att)
    await db.commit()
    await log_audit(db, user.id, "attachment.upload", "attachment", att.id,
                    {"name": att.name, "kind": kind, "sha256": att.file_sha256,
                     "size": len(content)})
    return {"id": att.id, **_serialize(att)}


@router.post("/convert-pdf")
async def convert_html_to_pdf(template_id: int = Query(..., description="HTML template to render"),
                              db: AsyncSession = Depends(get_db),
                              user: User = Depends(get_current_user),
                              _: None = Depends(require_csrf)):
    """Convert one of the user's saved HTML templates into a real PDF attachment
    using WeasyPrint. The generated PDF is stored and can be attached to campaigns."""
    tpl = await db.get(EmailTemplate, template_id)
    if not tpl or tpl.user_id != user.id:
        raise HTTPException(status_code=404, detail="Template not found")

    # Substitute variables with real sample values before validation/rendering
    # (mirrors the template test-pdf flow), so the generated PDF contains
    # rendered values instead of raw %Tokens%.
    from app.services.variable_engine import GeneratedValues, VariableEngine

    engine = VariableEngine({})
    g = GeneratedValues(invoice_number=engine.generate_invoice_number(set()),
                        amount=engine.generate_amount(),
                        shipping=engine.generate_shipping(),
                        invoice_date="2026-01-15", due_date="2026-01-30")
    res = engine.substitute(tpl.body_html, {"first_name": "Sample", "last_name": "Customer",
                                            "email": "sample@example.com",
                                            "invoice_number": g.invoice_number}, g,
                            extra={"unsubscribe_link": "https://example.com/unsubscribe"})
    problems = validate_html(res.text)
    # Optional variables (e.g. %CompanyPhone%) legitimately remain
    # unsubstituted in the sample render — same soft-warning filter the
    # upload endpoint applies. Only hard errors block conversion.
    problems = [p for p in problems if not p.startswith("Unsubstituted variables")]
    if problems:  # list of problem strings
        raise HTTPException(status_code=400,
                            detail=f"Template has errors: {'; '.join(problems)}")

    pdf_bytes = await asyncio.get_event_loop().run_in_executor(None, html_to_pdf, res.text)
    if not pdf_bytes:
        raise HTTPException(status_code=500, detail="PDF rendering failed")

    verdict, reason = att_svc.scan_for_malware(pdf_bytes, "converted.pdf")
    if not verdict:
        raise HTTPException(status_code=500, detail=f"PDF failed safety scan: {reason}")

    base = re.sub(r"\.[^.]+$", "", tpl.name or "attachment") or "attachment"
    fname = f"{base}.pdf"
    stored = att_svc.save_upload(fname, pdf_bytes)
    att = Attachment(
        user_id=user.id,
        name=f"{tpl.name} (PDF)"[:300],
        filename=fname,
        stored_path=stored["stored_path"],
        mime_type="application/pdf",
        size_bytes=len(pdf_bytes),
        kind="pdf",
        description=f"Generated from template '{tpl.name}' via WeasyPrint",
        html_source_template_id=tpl.id,
        file_sha256=hashlib.sha256(pdf_bytes).hexdigest(),
    )
    db.add(att)
    await db.commit()
    await log_audit(db, user.id, "attachment.convert_pdf", "html_template", tpl.id,
                    {"pdf_attachment_id": att.id, "size": len(pdf_bytes)})
    return {"id": att.id, **_serialize(att)}


@router.get("/{att_id}")
async def get_attachment(att_id: int, user: User = Depends(get_current_user),
                         db: AsyncSession = Depends(get_db)):
    att = await _get_own(db, user, att_id)
    out = _serialize(att)
    if att.is_html_attachment:
        out["html_body"] = att.html_attachment_body
    return out


@router.get("/{att_id}/download")
async def download_attachment(att_id: int, user: User = Depends(get_current_user),
                              db: AsyncSession = Depends(get_db)):
    att = await _get_own(db, user, att_id)
    if att.is_html_attachment or not att.stored_path:
        # inline HTML attachment: return as a file download
        from fastapi.responses import Response
        content = (att.html_attachment_body or "").encode("utf-8")
        return Response(content, media_type="text/html",
                        headers={"Content-Disposition": f'attachment; filename="{att.filename}"'})
    loop = asyncio.get_event_loop()
    path = await loop.run_in_executor(None, att_svc.safe_join, att.stored_path)
    if not path:
        raise HTTPException(status_code=404, detail="Stored file missing")
    return FileResponse(path, filename=att.filename, media_type=att.mime_type or "application/octet-stream")


@router.get("/{att_id}/preview")
async def preview_attachment(att_id: int, user: User = Depends(get_current_user),
                             db: AsyncSession = Depends(get_db)):
    """Preview: images and PDFs return base64 data URI; text/html return content."""
    att = await _get_own(db, user, att_id)

    if att.is_html_attachment:
        body = att.html_attachment_body or ""
        return {"previewable": True, "kind": "html",
                "content": sanitize_preview(body)[:50000]}

    content = await asyncio.get_event_loop().run_in_executor(
        None, lambda: att_svc.read_attachment_bytes(att) or b"")
    if not content:
        return {"previewable": False, "reason": "no_content"}
    if len(content) > 2 * 1024 * 1024:
        return {"previewable": False, "reason": "too_large", "size": len(content)}
    if att.kind in ("image", "pdf"):
        mime = att.mime_type or ("application/pdf" if att.kind == "pdf" else "image/png")
        return {"previewable": True, "kind": att.kind,
                "data_uri": f"data:{mime};base64,{base64.b64encode(content).decode()}"}
    if att.kind in ("text", "html", "file"):
        try:
            text = content.decode("utf-8")
        except UnicodeDecodeError:
            return {"previewable": False, "reason": "binary"}
        if att.mime_type == "text/html":
            text = sanitize_preview(text)
        return {"previewable": True, "kind": "text", "content": text[:20000]}
    return {"previewable": False, "reason": "unsupported_kind"}


@router.post("/{att_id}/delete")
async def delete_attachment(att_id: int, db: AsyncSession = Depends(get_db),
                            user: User = Depends(get_current_user),
                            _: None = Depends(require_csrf)):
    att = await _get_own(db, user, att_id)
    att_name = att.name
    if att.stored_path:
        await asyncio.get_event_loop().run_in_executor(
            None, att_svc.delete_attachment_file, att)
    await db.delete(att)
    await db.commit()
    await log_audit(db, user.id, "attachment.delete", "attachment", att_id,
                    {"name": att_name})
    return {"deleted": att_id}


@router.post("/clear-all")
async def clear_all_attachments(db: AsyncSession = Depends(get_db),
                                user: User = Depends(get_current_user),
                                _: None = Depends(require_csrf)):
    """DELETE ALL attachments for the user. Requires CSRF; UI shows explicit confirm dialog."""
    atts = (await db.execute(
        select(Attachment).where(Attachment.user_id == user.id))).scalars().all()
    loop = asyncio.get_event_loop()
    count = 0
    for a in atts:
        if a.stored_path:
            await loop.run_in_executor(None, att_svc.delete_attachment_file, a)
        await db.delete(a)
        count += 1
    await db.commit()
    await log_audit(db, user.id, "attachment.clear_all", "attachment", None,
                    {"deleted": count})
    return {"deleted": count}
