import uuid
from pathlib import Path

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

from ..config import settings
from ..db import get_session
from ..deps import require_user
from ..models import Attachment, File, Project, RecordingStep, Report, User, gen_share_token
from ..schemas import (
    AttachmentOut,
    ReportCreateIn,
    ReportDetailOut,
    ReportListItemOut,
    ReportUpdateIn,
    StepIn,
    StepOut,
)

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


async def get_report_by_token(token: str, db: AsyncSession) -> Report:
    result = await db.execute(select(Report).where(Report.share_token == token))
    report = result.scalar_one_or_none()
    if report is None:
        raise HTTPException(status.HTTP_404_NOT_FOUND, "Report not found")
    return report


async def get_report_for_edit(
    token: str, user: User = Depends(require_user), db: AsyncSession = Depends(get_session)
) -> Report:
    """Edit access: the report author or the project owner."""
    report = await get_report_by_token(token, db)
    project = await db.get(Project, report.project_id)
    if report.author_user_id != user.id and (project is None or project.owner_user_id != user.id):
        raise HTTPException(status.HTTP_403_FORBIDDEN, "Not allowed to modify this report")
    return report


@router.post("", response_model=ReportDetailOut, status_code=status.HTTP_201_CREATED)
async def create_report(
    body: ReportCreateIn, user: User = Depends(require_user), db: AsyncSession = Depends(get_session)
):
    project = await db.get(Project, body.project_id)
    if project is None or project.owner_user_id != user.id:
        raise HTTPException(status.HTTP_404_NOT_FOUND, "Project not found")

    # privacy enforcement server-side: never persist password inputs
    if body.steps:
        for step in body.steps:
            if step.type == "input" and (step.data or {}).get("input_type") == "password":
                step.data = {k: v for k, v in step.data.items() if k != "value"}
            elif step.type == "input" and isinstance((step.data or {}).get("value"), str):
                value = step.data["value"]
                if len(value) > settings.max_input_value_length:
                    step.data["value"] = value[: settings.max_input_value_length]
                    step.data["value_truncated"] = True

    report = Report(
        project_id=body.project_id,
        author_user_id=user.id,
        type=body.type,
        title=body.title,
        description=body.description,
        page_url=body.page_url,
        page_title=body.page_title,
        environment=body.environment.model_dump(mode="json") if body.environment else {},
        element=body.element.model_dump(mode="json") if body.element else None,
        mouse_track=body.mouse_track.model_dump(mode="json") if body.mouse_track else None,
    )
    db.add(report)
    await db.flush()

    # link uploaded files as attachments
    if body.attachment_ids:
        files = (
            await db.execute(select(File).where(File.id.in_(body.attachment_ids)))
        ).scalars().all()
        for file in files:
            mime = file.mime or ""
            if mime.startswith("image/"):
                kind = "screenshot"
            elif mime.startswith("video/"):
                kind = "video"
            else:
                kind = "document"
            shapes = (body.annotation_shapes or {}).get(file.id)
            db.add(
                Attachment(
                    report_id=report.id,
                    file_id=file.id,
                    kind=kind,
                    filename=file.path.rsplit("/", 1)[-1],
                    mime=file.mime,
                    size=file.size,
                    annotation_shapes=shapes,
                )
            )
        await db.flush()

    # link step screenshots (must already be uploaded and attached)
    if body.steps:
        attachments = (await db.execute(
            select(Attachment).where(Attachment.report_id == report.id)
        )).scalars().all()
        attachment_ids = {a.id for a in attachments}
        # clients reference the uploaded file id; attachment rows wrap files
        file_to_attachment = {a.file_id: a.id for a in attachments}
        for index, step in enumerate(body.steps):
            screenshot_id = None
            if step.attachment_id is not None:
                screenshot_id = (
                    step.attachment_id
                    if step.attachment_id in attachment_ids
                    else file_to_attachment.get(step.attachment_id)
                )
            db.add(
                RecordingStep(
                    report_id=report.id,
                    step_index=index,
                    type=step.type,
                    offset_ms=step.offset_ms,
                    data=step.data,
                    screenshot_attachment_id=screenshot_id,
                )
            )

    await db.commit()
    await db.refresh(report)
    return report


@router.get("/by-token/{token}/files/{file_id}")
async def download_report_file(token: str, file_id: uuid.UUID, db: AsyncSession = Depends(get_session)):
    """File download scoped under the report share token — the token IS the authorization."""
    report = await get_report_by_token(token, db)
    result = await db.execute(
        select(Attachment).where(Attachment.report_id == report.id, Attachment.file_id == file_id)
    )
    attachment = result.scalar_one_or_none()
    if attachment is None:
        raise HTTPException(status.HTTP_404_NOT_FOUND, "File not found")
    file = await db.get(File, attachment.file_id)
    path = settings.files_dir / file.path
    if not path.exists():
        raise HTTPException(status.HTTP_404_NOT_FOUND, "File missing on disk")
    return FileResponse(
        path,
        media_type=file.mime,
        filename=attachment.filename,
        headers={"Cache-Control": "private, max-age=3600"},
    )


@router.get("/{token}", response_model=ReportDetailOut)
async def get_report(token: str, db: AsyncSession = Depends(get_session)):
    report = await get_report_by_token(token, db)
    return ReportDetailOut(
        **ReportListItemOut.model_validate(report).model_dump(),
        attachments=[AttachmentOut.model_validate(a) for a in report.attachments],
        steps=[StepOut.model_validate(s) for s in report.steps],
    )


@router.patch("/{token}", response_model=ReportDetailOut)
async def update_report(
    body: ReportUpdateIn,
    report: Report = Depends(get_report_for_edit),
    db: AsyncSession = Depends(get_session),
):
    if body.title is not None:
        report.title = body.title
    if body.description is not None:
        report.description = body.description
    if body.status is not None:
        report.status = body.status
    await db.commit()
    await db.refresh(report)
    return await get_report(report.share_token, db)


@router.delete("/{token}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_report(
    report: Report = Depends(get_report_for_edit), db: AsyncSession = Depends(get_session)
):
    await db.delete(report)
    await db.commit()


@router.post("/{token}/share/rotate", response_model=ReportDetailOut)
async def rotate_report_share(
    report: Report = Depends(get_report_for_edit), db: AsyncSession = Depends(get_session)
):
    report.share_token = gen_share_token()
    await db.commit()
    await db.refresh(report)
    return await get_report(report.share_token, db)


@router.post("/{token}/steps", response_model=StepOut, status_code=status.HTTP_201_CREATED)
async def append_step(
    body: StepIn,
    report: Report = Depends(get_report_for_edit),
    db: AsyncSession = Depends(get_session),
):
    data = body.data or {}
    if body.type == "input" and data.get("input_type") == "password":
        data = {k: v for k, v in data.items() if k != "value"}
    elif body.type == "input" and isinstance(data.get("value"), str) and len(data["value"]) > settings.max_input_value_length:
        data["value"] = data["value"][: settings.max_input_value_length]
        data["value_truncated"] = True

    screenshot_id = None
    if body.attachment_id is not None:
        attachment = await db.get(Attachment, body.attachment_id)
        if attachment is not None and attachment.report_id == report.id:
            screenshot_id = attachment.id
    max_index = max((s.step_index for s in report.steps), default=-1)
    step = RecordingStep(
        report_id=report.id, step_index=max_index + 1, type=body.type, offset_ms=body.offset_ms,
        data=data, screenshot_attachment_id=screenshot_id,
    )
    db.add(step)
    await db.commit()
    await db.refresh(step)
    return step


@router.patch("/{token}/attachments/{attachment_id}", response_model=AttachmentOut)
async def update_attachment(
    attachment_id: uuid.UUID,
    annotation_shapes: list | None = None,
    report: Report = Depends(get_report_for_edit),
    db: AsyncSession = Depends(get_session),
):
    """Persist annotation vector shapes (they are re-rendered on the screenshot later)."""
    attachment = next((a for a in report.attachments if a.id == attachment_id), None)
    if attachment is None:
        raise HTTPException(status.HTTP_404_NOT_FOUND, "Attachment not found")
    if annotation_shapes is not None:
        attachment.annotation_shapes = annotation_shapes
    await db.commit()
    await db.refresh(attachment)
    return attachment


@router.delete("/{token}/steps/{step_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_step(
    token: str,
    step_id: uuid.UUID,
    report: Report = Depends(get_report_for_edit),
    db: AsyncSession = Depends(get_session),
):
    step = next((s for s in report.steps if s.id == step_id), None)
    if step is None:
        raise HTTPException(status.HTTP_404_NOT_FOUND, "Step not found")
    await db.delete(step)
    await db.commit()