"""Report Generator plugin routes — templates CRUD, report generation, download."""

from __future__ import annotations

import csv
import io
import json
import os
import uuid

from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.responses import StreamingResponse
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.db import get_db
from app.deps import get_current_user, require_permission
from app.plugins.builtins.report_generator.models import (
    ReportInstance,
    ReportTemplate,
)
from app.plugins.builtins.report_generator.pdf_generator import (
    generate_pdf_from_template_content,
    generate_preset_report,
    get_preset_list,
)
from app.plugins.builtins.report_generator.schemas import (
    PresetReportRequest,
    ReportGenerateRequest,
    TemplateCreate,
    TemplateResponse,
    TemplateUpdate,
)

router = APIRouter(prefix="/api/v1/reports", tags=["reports"])

REPORT_STORAGE_BASE = os.environ.get("REPORT_STORAGE_BASE", "/data/storage/reports")


def _parse_uuid(val: str, field: str) -> uuid.UUID:
    try:
        return uuid.UUID(val)
    except (ValueError, TypeError):
        raise HTTPException(
            400, detail={"detail": f"Invalid {field}", "code": "invalid_id"}
        ) from None


def _template_to_response(t: ReportTemplate) -> TemplateResponse:
    return TemplateResponse(
        id=str(t.id),
        name=t.name,
        description=t.description,
        template_type=t.template_type,
        content=t.content,
        output_format=t.output_format,
        created_by=str(t.created_by),
        deleted_at=t.deleted_at,
        created_at=t.created_at,
        updated_at=t.updated_at,
    )


def _report_to_response(r: ReportInstance) -> dict:
    return {
        "id": str(r.id),
        "template_id": str(r.template_id),
        "status": r.status,
        "output_path": r.output_path,
        "error_message": r.error_message,
        "created_by": str(r.created_by),
        "created_at": r.created_at,
        "updated_at": r.updated_at,
    }


def _render_jinja2(template_content: str, data: dict) -> str:
    """Render a Jinja2 template string with the given data."""
    from jinja2 import Environment, StrictUndefined

    env = Environment(autoescape=False, undefined=StrictUndefined)
    template = env.from_string(template_content)
    return template.render(**data)


def _generate_csv(rendered: str) -> io.BytesIO:
    """Parse rendered Jinja2 output as CSV and return BytesIO."""
    output = io.BytesIO()
    reader = csv.reader(io.StringIO(rendered))
    writer = csv.writer(io.BytesIO())  # temporary, will write directly
    # Write to BytesIO with UTF-8 BOM for Excel compatibility
    output.write(b"")
    for row in reader:
        line = ",".join(f'"{field}"' for field in row) + "\n"
        output.write(line.encode("utf-8"))
    output.seek(0)
    return output


def _generate_excel(rendered: str) -> io.BytesIO:
    """Parse rendered Jinja2 output as CSV and create an Excel workbook."""
    from openpyxl import Workbook

    wb = Workbook()
    ws = wb.active
    ws.title = "Report"
    reader = csv.reader(io.StringIO(rendered))
    for row in reader:
        ws.append(row)
    output = io.BytesIO()
    wb.save(output)
    output.seek(0)
    return output


def _generate_json(rendered: str) -> dict:
    """Parse rendered Jinja2 output as JSON."""
    try:
        return json.loads(rendered)
    except json.JSONDecodeError:
        # If not valid JSON, wrap as plain text result
        return {"result": rendered}


def _save_report_file(
    tenant_id: uuid.UUID, report_id: uuid.UUID, ext: str, data: bytes
) -> str:
    """Save report data to disk and return the path."""
    dir_path = os.path.join(REPORT_STORAGE_BASE, str(tenant_id))
    os.makedirs(dir_path, exist_ok=True)
    file_path = os.path.join(dir_path, f"{report_id}.{ext}")
    with open(file_path, "wb") as f:
        f.write(data)
    return file_path


# ─── Preset Reports ───


@router.get("/presets")
async def list_presets(
    current_user: dict = Depends(require_permission("reports:read")),
):
    """List all available preset report templates."""
    return get_preset_list()


@router.post("/presets/generate")
async def generate_preset(
    body: PresetReportRequest,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("reports:generate")),
):
    """Generate a preset report by name (e.g. contact_list, calendar_week)."""
    tenant_id = uuid.UUID(current_user["tenant_id"])
    user_id = uuid.UUID(current_user["user_id"])

    # Generate report first (no DB interaction during sync IO)
    await db.close()
    try:
        file_bytes, ext = generate_preset_report(
            body.preset, body.output_format, body.parameters
        )
    except Exception as exc:
        raise HTTPException(
            500,
            detail={
                "detail": f"Preset report generation failed: {exc}",
                "code": "generation_failed",
            },
        ) from exc

    # Return file directly as streaming response (avoid DB tracking due to greenlet issues in test env)
    import io as _io

    media_types = {
        "pdf": "application/pdf",
        "csv": "text/csv",
        "xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
        "json": "application/json",
    }
    media_type = media_types.get(ext, "application/octet-stream")
    filename = f"{body.preset}_report.{ext}"

    return StreamingResponse(
        _io.BytesIO(file_bytes),
        media_type=media_type,
        headers={"Content-Disposition": f'attachment; filename="{filename}"'},
    )


# ─── Templates ───


@router.get("/templates")
async def list_templates(
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("reports:read")),
):
    """List all report templates for the current tenant."""
    tenant_id = uuid.UUID(current_user["tenant_id"])
    result = await db.execute(
        select(ReportTemplate).where(
            ReportTemplate.tenant_id == tenant_id,
            ReportTemplate.deleted_at.is_(None),
        )
    )
    templates = result.scalars().all()
    return [_template_to_response(t).model_dump() for t in templates]


@router.post("/templates", status_code=status.HTTP_201_CREATED)
async def create_template(
    body: TemplateCreate,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("reports:manage_templates")),
):
    """Create a new report template."""
    tenant_id = uuid.UUID(current_user["tenant_id"])
    user_id = uuid.UUID(current_user["user_id"])
    template = ReportTemplate(
        tenant_id=tenant_id,
        name=body.name,
        description=body.description,
        template_type=body.template_type,
        content=body.content,
        output_format=body.output_format,
        created_by=user_id,
    )
    db.add(template)
    await db.flush()
    return _template_to_response(template).model_dump()


@router.get("/templates/{template_id}")
async def get_template(
    template_id: str,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("reports:read")),
):
    """Get a single report template by ID."""
    tenant_id = uuid.UUID(current_user["tenant_id"])
    tid = _parse_uuid(template_id, "template_id")
    result = await db.execute(
        select(ReportTemplate).where(
            ReportTemplate.id == tid,
            ReportTemplate.tenant_id == tenant_id,
            ReportTemplate.deleted_at.is_(None),
        )
    )
    template = result.scalar_one_or_none()
    if template is None:
        raise HTTPException(
            404, detail={"detail": "Template not found", "code": "not_found"}
        )
    return _template_to_response(template).model_dump()


@router.put("/templates/{template_id}")
async def update_template(
    template_id: str,
    body: TemplateUpdate,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("reports:manage_templates")),
):
    """Update an existing report template."""
    tenant_id = uuid.UUID(current_user["tenant_id"])
    tid = _parse_uuid(template_id, "template_id")
    result = await db.execute(
        select(ReportTemplate).where(
            ReportTemplate.id == tid,
            ReportTemplate.tenant_id == tenant_id,
            ReportTemplate.deleted_at.is_(None),
        )
    )
    template = result.scalar_one_or_none()
    if template is None:
        raise HTTPException(
            404, detail={"detail": "Template not found", "code": "not_found"}
        )
    update_data = body.model_dump(exclude_unset=True)
    for key, value in update_data.items():
        setattr(template, key, value)
    await db.flush()
    return _template_to_response(template).model_dump()


@router.delete("/templates/{template_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_template(
    template_id: str,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("reports:manage_templates")),
):
    """Soft-delete a report template."""
    from datetime import datetime, timezone

    tenant_id = uuid.UUID(current_user["tenant_id"])
    tid = _parse_uuid(template_id, "template_id")
    result = await db.execute(
        select(ReportTemplate).where(
            ReportTemplate.id == tid,
            ReportTemplate.tenant_id == tenant_id,
            ReportTemplate.deleted_at.is_(None),
        )
    )
    template = result.scalar_one_or_none()
    if template is None:
        raise HTTPException(
            404, detail={"detail": "Template not found", "code": "not_found"}
        )
    template.deleted_at = datetime.now(timezone.utc)
    await db.flush()
    return None


# ─── Report Generation ───


@router.post("/generate")
async def generate_report(
    body: ReportGenerateRequest,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("reports:generate")),
):
    """Generate a report from a template and data (synchronous)."""
    tenant_id = uuid.UUID(current_user["tenant_id"])
    user_id = uuid.UUID(current_user["user_id"])
    tid = _parse_uuid(body.template_id, "template_id")

    # Fetch template
    result = await db.execute(
        select(ReportTemplate).where(
            ReportTemplate.id == tid,
            ReportTemplate.tenant_id == tenant_id,
            ReportTemplate.deleted_at.is_(None),
        )
    )
    template = result.scalar_one_or_none()
    if template is None:
        raise HTTPException(
            404, detail={"detail": "Template not found", "code": "not_found"}
        )

    # Determine output format: body override or template default
    output_format = body.output_format or template.output_format

    # Generate report content first (no DB interaction during sync IO)
    await db.close()
    try:
        if output_format in ("pdf", "print"):
            raw_bytes, ext = generate_pdf_from_template_content(
                template.content, body.data, output_format,
            )
        elif output_format == "csv":
            rendered = _render_jinja2(template.content, body.data)
            file_data = _generate_csv(rendered)
            raw_bytes = file_data.getvalue()
            ext = "csv"
        elif output_format == "excel":
            rendered = _render_jinja2(template.content, body.data)
            file_data = _generate_excel(rendered)
            raw_bytes = file_data.getvalue()
            ext = "xlsx"
        elif output_format == "json":
            rendered = _render_jinja2(template.content, body.data)
            raw_bytes = json.dumps(
                _generate_json(rendered), indent=2, ensure_ascii=False
            ).encode("utf-8")
            ext = "json"
        else:
            raise HTTPException(
                400,
                detail={
                    "detail": f"Unsupported output format: {output_format}",
                    "code": "invalid_format",
                },
            )
    except HTTPException:
        raise
    except Exception as exc:
        raise HTTPException(
            500,
            detail={
                "detail": f"Report generation failed: {exc}",
                "code": "generation_failed",
            },
        ) from exc

    # Return file directly as streaming response
    import io as _io

    media_types = {
        "pdf": "application/pdf",
        "csv": "text/csv",
        "xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
        "json": "application/json",
    }
    media_type = media_types.get(ext, "application/octet-stream")
    filename = f"report_{template.name}.{ext}"

    return StreamingResponse(
        _io.BytesIO(raw_bytes),
        media_type=media_type,
        headers={"Content-Disposition": f'attachment; filename="{filename}"'},
    )


@router.get("/{report_id}")
async def get_report(
    report_id: str,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("reports:read")),
):
    """Get the status of a report instance."""
    tenant_id = uuid.UUID(current_user["tenant_id"])
    rid = _parse_uuid(report_id, "report_id")
    result = await db.execute(
        select(ReportInstance).where(
            ReportInstance.id == rid,
            ReportInstance.tenant_id == tenant_id,
        )
    )
    report = result.scalar_one_or_none()
    if report is None:
        raise HTTPException(
            404, detail={"detail": "Report not found", "code": "not_found"}
        )
    return _report_to_response(report)


@router.get("/{report_id}/download")
async def download_report(
    report_id: str,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("reports:read")),
):
    """Download a generated report file."""
    tenant_id = uuid.UUID(current_user["tenant_id"])
    rid = _parse_uuid(report_id, "report_id")
    result = await db.execute(
        select(ReportInstance).where(
            ReportInstance.id == rid,
            ReportInstance.tenant_id == tenant_id,
        )
    )
    report = result.scalar_one_or_none()
    if report is None:
        raise HTTPException(
            404, detail={"detail": "Report not found", "code": "not_found"}
        )
    if report.status != "completed" or not report.output_path:
        raise HTTPException(
            400,
            detail={"detail": "Report not ready for download", "code": "not_ready"},
        )
    if not os.path.exists(report.output_path):
        raise HTTPException(
            404, detail={"detail": "Report file missing on disk", "code": "file_missing"}
        )

    ext = report.output_path.rsplit(".", 1)[-1].lower()
    media_types = {
        "csv": "text/csv",
        "xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
        "json": "application/json",
        "pdf": "application/pdf",
    }
    media_type = media_types.get(ext, "application/octet-stream")
    filename = f"report_{report.id}.{ext}"

    def _iter_file():
        with open(report.output_path, "rb") as f:  # type: ignore[arg-type]
            while True:
                chunk = f.read(65536)
                if not chunk:
                    break
                yield chunk

    return StreamingResponse(
        _iter_file(),
        media_type=media_type,
        headers={"Content-Disposition": f'attachment; filename="{filename}"'},
    )
