"""Mail plugin routes — accounts, folders, mails, send, search, threads,
templates, signatures, rules, vacation, PGP, labels, delegates.

Route ordering: all static-path routes are registered BEFORE dynamic /{mail_id} routes
so that GET /search, /threads, /templates etc. are not shadowed by GET /{mail_id}.
"""

from __future__ import annotations

import json
import os
import uuid
from datetime import UTC, datetime

from fastapi import APIRouter, Body, Depends, File, HTTPException, Query, UploadFile
from fastapi.responses import StreamingResponse
from sqlalchemy import and_, asc, desc, func, or_, select
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.db import get_db
from app.core.storage import get_storage_backend
from app.deps import require_permission
from app.plugins.builtins.mail.models import (
    ContactPgpKey,
    Mail,
    MailAccount,
    MailAccountDelegate,
    MailAccountSendPermission,
    MailAttachment,
    MailFolder,
    MailLabel,
    MailLabelAssignment,
    MailRule,
    MailSignature,
    MailSyncQueue,
    MailTemplate,
    PgpKey,
)
from app.plugins.builtins.mail.schemas import (
    DelegateCreate,
    MailAccountCreate,
    MailAccountUpdate,
    MailCreateEventRequest,
    MailDraftCreate,
    MailFlagsUpdate,
    MailFolderCreate,
    MailFolderUpdate,
    MailForwardRequest,
    MailLabelAssign,
    MailLabelCreate,
    MailLinkRequest,
    MailMoveRequest,
    MailReplyRequest,
    MailRuleCreate,
    MailSendRequest,
    MailSignatureCreate,
    MailTemplateCreate,
    PgpKeyImport,
    SendPermissionCreate,
    SharedMailboxUserAssign,
    TemplateSubstituteRequest,
    VacationConfig,
)
import app.plugins.builtins.mail.services as mail_services
from app.plugins.builtins.mail.services import (
    MAX_ATTACHMENT_SIZE,
    _attachment_storage_path,
    _sanitize_filename,
    _save_attachment_to_storage,
    account_to_response,
    apply_rules_to_mail,
    attachment_to_response,
    create_mail_account,
    encrypt_password,
    folder_to_response,
    forward_mail,
    get_account_password,
    imap_create_folder,
    imap_delete_folder,
    imap_delete_mail,
    imap_move_mail,
    imap_sync_account,
    import_pgp_private_key,
    import_pgp_public_key,
    label_to_response,
    log_vacation_sent,
    mail_to_response,
    pgp_encrypt_message,
    reply_to_mail,
    rule_to_response,
    save_draft,
    send_mail_via_smtp,
    should_send_vacation_reply,
    signature_to_response,
    substitute_template_vars,
    template_to_response,
    update_draft,
    update_mail_account,
)

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


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


async def _resolve_attachment_paths(attachment_ids: list[str]) -> list[dict]:
    """Resolve temporary attachment upload IDs to file paths on disk.

    Each uploaded attachment is stored under
    ``<storage>/mail_uploads/<temp_id>/<filename>``.  We scan the
    directory for the single file inside and return its metadata.
    """
    resolved: list[dict] = []
    storage = get_storage_backend()
    for att_id in attachment_ids:
        prefix = f"mail_uploads/{att_id}/"
        files = await storage.list_files(prefix)
        if not files:
            continue
        for fpath in files:
            fname = os.path.basename(fpath)
            abs_path = await storage.get_url(fpath)
            resolved.append(
                {
                    "path": abs_path,
                    "filename": fname,
                    "mime_type": "application/octet-stream",
                }
            )
            break
    return resolved


async def _get_account(
    db: AsyncSession, account_id: uuid.UUID, tenant_id: uuid.UUID, user_id: uuid.UUID
) -> MailAccount:
    account = (
        await db.execute(
            select(MailAccount).where(
                and_(MailAccount.id == account_id, MailAccount.tenant_id == tenant_id)
            )
        )
    ).scalar_one_or_none()
    if not account:
        raise HTTPException(404, detail={"detail": "Mail account not found", "code": "not_found"})
    if account.user_id == user_id:
        return account
    delegate = (
        await db.execute(
            select(MailAccountDelegate).where(
                and_(
                    MailAccountDelegate.account_id == account_id,
                    MailAccountDelegate.delegate_user_id == user_id,
                )
            )
        )
    ).scalar_one_or_none()
    if delegate:
        return account
    raise HTTPException(403, detail={"detail": "No access to this account", "code": "forbidden"})


async def _check_send_permission(
    db: AsyncSession, account: MailAccount, user_id: uuid.UUID
) -> None:
    if account.user_id == user_id:
        return
    perm = (
        await db.execute(
            select(MailAccountSendPermission).where(
                and_(
                    MailAccountSendPermission.account_id == account.id,
                    MailAccountSendPermission.user_id == user_id,
                )
            )
        )
    ).scalar_one_or_none()
    if not perm:
        raise HTTPException(403, detail={"detail": "No send permission", "code": "forbidden"})


_LEVEL_HIERARCHY = {"read": 1, "write": 2, "delete": 3, "full": 4}


async def _check_delegate_access(
    db: AsyncSession, account: MailAccount, user_id: uuid.UUID, required_level: str = "read"
) -> None:
    """Check if user has delegate access to account with at least the required level."""
    if account.user_id == user_id:
        return  # Owner has full access
    delegate = (
        await db.execute(
            select(MailAccountDelegate).where(
                and_(
                    MailAccountDelegate.account_id == account.id,
                    MailAccountDelegate.delegate_user_id == user_id,
                )
            )
        )
    ).scalar_one_or_none()
    if not delegate:
        raise HTTPException(403, detail={"detail": "No delegate access to this mailbox", "code": "forbidden"})
    user_level = _LEVEL_HIERARCHY.get(delegate.access_level, 0)
    required = _LEVEL_HIERARCHY.get(required_level, 1)
    if user_level < required:
        raise HTTPException(
            403,
            detail={"detail": f"Insufficient delegate access (need {required_level}, have {delegate.access_level})", "code": "forbidden"}
        )


# ═══════════════════════════════════════════════════════════════
# STATIC ROUTES — registered first so they are not shadowed
# ═══════════════════════════════════════════════════════════════

# ─── Mail Accounts (F-MAIL-14, F-MAIL-18) ───


@router.get("/accounts")
async def list_accounts(
    db: AsyncSession = Depends(get_db), current_user: dict = Depends(require_permission("mail:read"))
):
    tenant_id = uuid.UUID(current_user["tenant_id"])
    user_id = uuid.UUID(current_user["user_id"])
    accounts = (
        (
            await db.execute(
                select(MailAccount).where(
                    and_(
                        MailAccount.tenant_id == tenant_id,
                        or_(MailAccount.user_id == user_id, MailAccount.is_shared),
                    )
                )
            )
        )
        .scalars()
        .all()
    )
    return [account_to_response(a) for a in accounts]


@router.post("/accounts", status_code=201)
async def create_account(
    data: MailAccountCreate,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("mail:config")),
):
    tenant_id = uuid.UUID(current_user["tenant_id"])
    user_id = uuid.UUID(current_user["user_id"])
    account = await create_mail_account(
        db, tenant_id=tenant_id, user_id=user_id, data=data.model_dump()
    )
    return account_to_response(account)


@router.get("/accounts/shared")
async def list_shared_accounts(
    db: AsyncSession = Depends(get_db), current_user: dict = Depends(require_permission("mail:read"))
):
    tenant_id = uuid.UUID(current_user["tenant_id"])
    accounts = (
        (
            await db.execute(
                select(MailAccount).where(
                    and_(MailAccount.tenant_id == tenant_id, MailAccount.is_shared)
                )
            )
        )
        .scalars()
        .all()
    )
    return [account_to_response(a) for a in accounts]


@router.patch("/accounts/{account_id}")
async def update_account(
    account_id: str,
    data: MailAccountUpdate,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("mail:config")),
):
    tenant_id = uuid.UUID(current_user["tenant_id"])
    user_id = uuid.UUID(current_user["user_id"])
    acc_id = _parse_uuid(account_id, "account_id")
    account = await _get_account(db, acc_id, tenant_id, user_id)
    await update_mail_account(db, account, data.model_dump(exclude_unset=True))
    return account_to_response(account)


@router.delete("/accounts/{account_id}", status_code=204)
async def delete_account(
    account_id: str,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("mail:config")),
):
    tenant_id = uuid.UUID(current_user["tenant_id"])
    user_id = uuid.UUID(current_user["user_id"])
    acc_id = _parse_uuid(account_id, "account_id")
    account = await _get_account(db, acc_id, tenant_id, user_id)
    if account.user_id != user_id:
        raise HTTPException(403, detail={"detail": "Only owner can delete", "code": "forbidden"})
    await db.delete(account)


@router.post("/accounts/{account_id}/users")
async def assign_shared_users(
    account_id: str,
    data: SharedMailboxUserAssign,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("mail:share")),
):
    tenant_id = uuid.UUID(current_user["tenant_id"])
    user_id = uuid.UUID(current_user["user_id"])
    acc_id = _parse_uuid(account_id, "account_id")
    account = await _get_account(db, acc_id, tenant_id, user_id)
    if account.user_id != user_id:
        raise HTTPException(
            403, detail={"detail": "Only owner can assign users", "code": "forbidden"}
        )
    assigned = []
    for uid_str in data.user_ids:
        uid = _parse_uuid(uid_str, "user_id")
        existing = (
            await db.execute(
                select(MailAccountDelegate).where(
                    and_(
                        MailAccountDelegate.account_id == acc_id,
                        MailAccountDelegate.delegate_user_id == uid,
                    )
                )
            )
        ).scalar_one_or_none()
        if not existing:
            db.add(
                MailAccountDelegate(
                    tenant_id=tenant_id,
                    account_id=acc_id,
                    delegate_user_id=uid,
                    access_level="read",
                )
            )
            assigned.append(str(uid))
    await db.flush()
    return {"assigned": assigned}


@router.post("/accounts/{account_id}/delegates")
async def create_delegate(
    account_id: str,
    data: DelegateCreate,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("mail:share")),
):
    tenant_id = uuid.UUID(current_user["tenant_id"])
    user_id = uuid.UUID(current_user["user_id"])
    acc_id = _parse_uuid(account_id, "account_id")
    account = await _get_account(db, acc_id, tenant_id, user_id)
    if account.user_id != user_id:
        raise HTTPException(
            403, detail={"detail": "Only owner can create delegates", "code": "forbidden"}
        )
    delegate_uid = _parse_uuid(data.delegate_user_id, "delegate_user_id")
    existing = (
        await db.execute(
            select(MailAccountDelegate).where(
                and_(
                    MailAccountDelegate.account_id == acc_id,
                    MailAccountDelegate.delegate_user_id == delegate_uid,
                )
            )
        )
    ).scalar_one_or_none()
    if existing:
        existing.access_level = data.access_level
        await db.flush()
        return {"id": str(existing.id), "access_level": existing.access_level}
    delegate = MailAccountDelegate(
        tenant_id=tenant_id,
        account_id=acc_id,
        delegate_user_id=delegate_uid,
        access_level=data.access_level,
    )
    db.add(delegate)
    await db.flush()
    return {"id": str(delegate.id), "access_level": delegate.access_level}


@router.post("/accounts/{account_id}/send-permissions")
async def create_send_permission(
    account_id: str,
    data: SendPermissionCreate,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("mail:share")),
):
    tenant_id = uuid.UUID(current_user["tenant_id"])
    user_id = uuid.UUID(current_user["user_id"])
    acc_id = _parse_uuid(account_id, "account_id")
    account = await _get_account(db, acc_id, tenant_id, user_id)
    if account.user_id != user_id:
        raise HTTPException(
            403, detail={"detail": "Only owner can grant send permissions", "code": "forbidden"}
        )
    perm_uid = _parse_uuid(data.user_id, "user_id")
    existing = (
        await db.execute(
            select(MailAccountSendPermission).where(
                and_(
                    MailAccountSendPermission.account_id == acc_id,
                    MailAccountSendPermission.user_id == perm_uid,
                )
            )
        )
    ).scalar_one_or_none()
    if existing:
        return {"id": str(existing.id), "user_id": str(perm_uid)}
    perm = MailAccountSendPermission(tenant_id=tenant_id, account_id=acc_id, user_id=perm_uid)
    db.add(perm)
    await db.flush()
    return {"id": str(perm.id), "user_id": str(perm_uid)}


@router.post("/accounts/{account_id}/test-connection")
async def test_connection(
    account_id: str,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("mail:config")),
):
    tenant_id = uuid.UUID(current_user["tenant_id"])
    user_id = uuid.UUID(current_user["user_id"])
    acc_id = _parse_uuid(account_id, "account_id")
    account = await _get_account(db, acc_id, tenant_id, user_id)
    password = await get_account_password(account)
    try:
        import aioimaplib

        client = aioimaplib.IMAP4_SSL(host=account.imap_host, port=account.imap_port)
        await client.wait_hello_from_server()
        await client.login(account.username, password)
        await client.logout()
        imap_ok = True
    except Exception:
        imap_ok = False
    try:
        import aiosmtplib

        smtp = aiosmtplib.SMTP(
            hostname=account.smtp_host, port=account.smtp_port, use_tls=account.smtp_tls
        )
        await smtp.connect()
        await smtp.login(account.username, password)
        await smtp.quit()
        smtp_ok = True
    except Exception:
        smtp_ok = False
    return {"imap_ok": imap_ok, "smtp_ok": smtp_ok, "message": "Connection test complete"}


@router.post("/accounts/{account_id}/sync")
async def trigger_imap_sync(
    account_id: str,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("mail:config")),
):
    tenant_id = uuid.UUID(current_user["tenant_id"])
    user_id = uuid.UUID(current_user["user_id"])
    acc_id = _parse_uuid(account_id, "account_id")
    await _get_account(db, acc_id, tenant_id, user_id)
    result = await imap_sync_account(db, acc_id, tenant_id)
    return result


# ─── Mail Folders (F-MAIL-01, F-MAIL-19) ───


@router.get("/folders")
async def list_folders(
    account_id: str = Query(...),
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("mail:read")),
):
    tenant_id = uuid.UUID(current_user["tenant_id"])
    user_id = uuid.UUID(current_user["user_id"])
    acc_id = _parse_uuid(account_id, "account_id")
    await _get_account(db, acc_id, tenant_id, user_id)
    folders = (
        (
            await db.execute(
                select(MailFolder)
                .where(and_(MailFolder.account_id == acc_id, MailFolder.tenant_id == tenant_id))
                .order_by(MailFolder.is_standard.desc(), MailFolder.name)
            )
        )
        .scalars()
        .all()
    )
    return [folder_to_response(f) for f in folders]


@router.post("/folders", status_code=201)
async def create_folder(
    data: MailFolderCreate,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("mail:config")),
):
    tenant_id = uuid.UUID(current_user["tenant_id"])
    user_id = uuid.UUID(current_user["user_id"])
    acc_id = _parse_uuid(data.account_id, "account_id")
    await _get_account(db, acc_id, tenant_id, user_id)
    parent_id = _parse_uuid(data.parent_id, "parent_id") if data.parent_id else None
    folder = MailFolder(
        tenant_id=tenant_id,
        account_id=acc_id,
        name=data.name,
        imap_name=data.imap_name,
        parent_id=parent_id,
        is_standard=False,
    )
    db.add(folder)
    await db.flush()
    try:
        await imap_create_folder(db, acc_id, data.imap_name, tenant_id)
    except Exception:
        import logging as _logging

        _logging.getLogger(__name__).warning(
            "create_folder: IMAP CREATE failed (non-critical) for %s", data.imap_name
        )
    return folder_to_response(folder)


@router.patch("/folders/{folder_id}")
async def update_folder(
    folder_id: str,
    data: MailFolderUpdate,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("mail:config")),
):
    tenant_id = uuid.UUID(current_user["tenant_id"])
    user_id = uuid.UUID(current_user["user_id"])
    f_id = _parse_uuid(folder_id, "folder_id")
    folder = (
        await db.execute(
            select(MailFolder).where(and_(MailFolder.id == f_id, MailFolder.tenant_id == tenant_id))
        )
    ).scalar_one_or_none()
    if not folder:
        raise HTTPException(404, detail={"detail": "Folder not found", "code": "not_found"})
    await _get_account(db, folder.account_id, tenant_id, user_id)
    if data.name:
        folder.name = data.name
    await db.flush()
    return folder_to_response(folder)


@router.delete("/folders/{folder_id}", status_code=204)
async def delete_folder(
    folder_id: str,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("mail:config")),
):
    tenant_id = uuid.UUID(current_user["tenant_id"])
    user_id = uuid.UUID(current_user["user_id"])
    f_id = _parse_uuid(folder_id, "folder_id")
    folder = (
        await db.execute(
            select(MailFolder).where(and_(MailFolder.id == f_id, MailFolder.tenant_id == tenant_id))
        )
    ).scalar_one_or_none()
    if not folder:
        raise HTTPException(404, detail={"detail": "Folder not found", "code": "not_found"})
    account = await _get_account(db, folder.account_id, tenant_id, user_id)
    await _check_delegate_access(db, account, user_id, "full")
    try:
        await imap_delete_folder(db, f_id, tenant_id)
    except Exception:
        import logging as _logging

        _logging.getLogger(__name__).warning(
            "delete_folder: IMAP DELETE failed (non-critical) for folder %s", f_id
        )
    await db.delete(folder)


# ─── Empty Folder (move to Trash or permanent delete) ───


@router.post("/folders/{folder_id}/empty")
async def empty_folder(
    folder_id: str,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("mail:delete")),
):
    """Empty a folder: move all mails to Trash, or permanent delete if already in Trash.

    If the folder IS the Trash folder, mails are permanently deleted (DELETE + IMAP EXPUNGE).
    If the folder is NOT Trash, mails are moved to Trash (folder_id changed + IMAP MOVE).
    Returns the number of mails that were emptied.
    """
    tenant_id = uuid.UUID(current_user["tenant_id"])
    user_id = uuid.UUID(current_user["user_id"])
    f_id = _parse_uuid(folder_id, "folder_id")
    folder = (
        await db.execute(
            select(MailFolder).where(and_(MailFolder.id == f_id, MailFolder.tenant_id == tenant_id))
        )
    ).scalar_one_or_none()
    if not folder:
        raise HTTPException(404, detail={"detail": "Folder not found", "code": "not_found"})
    account = await _get_account(db, folder.account_id, tenant_id, user_id)
    await _check_delegate_access(db, account, user_id, "delete")

    # Check if this folder IS the Trash folder
    is_trash = 'trash' in folder.imap_name.lower() or 'papierkorb' in folder.name.lower() or 'trash' in folder.name.lower()

    # Find Trash folder for non-trash emptying
    trash_folder = None
    if not is_trash:
        all_folders = (
            await db.execute(
                select(MailFolder).where(
                    and_(MailFolder.account_id == folder.account_id, MailFolder.tenant_id == tenant_id)
                )
            )
        ).scalars().all()
        for f in all_folders:
            if 'trash' in f.imap_name.lower() or 'papierkorb' in f.name.lower() or 'trash' in f.name.lower():
                trash_folder = f
                break

    # Get all mails in this folder
    result = await db.execute(
        select(Mail).where(
            and_(
                Mail.folder_id == f_id,
                Mail.tenant_id == tenant_id,
            )
        )
    )
    mails = result.scalars().all()
    for mail in mails:
        if is_trash or not trash_folder:
            # Permanent delete from DB + IMAP EXPUNGE
            # IMPORTANT: Call imap_delete_mail BEFORE db.delete, because imap_delete_mail
            # reads the mail from DB to find its folder and imap_uid.
            try:
                await mail_services.imap_delete_mail(db, mail.id, tenant_id, permanent=True)
                await db.delete(mail)
            except Exception as exc:
                import logging
                logging.getLogger(__name__).warning(
                    "empty_folder: IMAP permanent delete failed for mail %s: %s, queuing for retry", mail.id, exc
                )
                await db.delete(mail)
                queue_entry = MailSyncQueue(
                    tenant_id=tenant_id,
                    mail_id=mail.id,
                    operation="delete",
                    payload={"permanent": True},
                    status="pending",
                    last_error=str(exc),
                )
                db.add(queue_entry)
        else:
            # Move to Trash folder (standard mail program logic)
            # IMPORTANT: Call imap_delete_mail BEFORE changing folder_id,
            # because imap_delete_mail reads mail.folder_id to find the IMAP folder.
            try:
                await mail_services.imap_delete_mail(db, mail.id, tenant_id, permanent=False)
                mail.folder_id = trash_folder.id
            except Exception as exc:
                import logging
                logging.getLogger(__name__).warning(
                    "empty_folder: IMAP move to Trash failed for mail %s: %s, queuing for retry", mail.id, exc
                )
                mail.folder_id = trash_folder.id
                queue_entry = MailSyncQueue(
                    tenant_id=tenant_id,
                    mail_id=mail.id,
                    operation="delete",
                    payload={"permanent": False},
                    status="pending",
                    last_error=str(exc),
                )
                db.add(queue_entry)
    await db.flush()
    return {"emptied_count": len(mails)}


# ─── Sync single folder (live sync when selecting a folder) ───


@router.post("/folders/{folder_id}/sync")
async def sync_folder(
    folder_id: str,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("mail:read")),
):
    """Sync a single folder from IMAP server immediately."""
    tenant_id = uuid.UUID(current_user["tenant_id"])
    user_id = uuid.UUID(current_user["user_id"])
    f_id = _parse_uuid(folder_id, "folder_id")
    folder = (
        await db.execute(
            select(MailFolder).where(and_(MailFolder.id == f_id, MailFolder.tenant_id == tenant_id))
        )
    ).scalar_one_or_none()
    if not folder:
        raise HTTPException(404, detail={"detail": "Folder not found", "code": "not_found"})
    account = await _get_account(db, folder.account_id, tenant_id, user_id)
    result = await mail_services.imap_sync_folder(db, f_id, tenant_id)
    await db.flush()
    return result


# ─── Attachment Upload (F-MAIL-04) ───


@router.post("/upload-attachment")
async def upload_attachment(
    file: UploadFile = File(...),
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("mail:write")),
):
    """Upload a file to be attached to an outgoing email.

    Returns a temporary attachment ID that can be passed in the
    `attachments` list of the send-mail request.
    """
    tenant_id = uuid.UUID(current_user["tenant_id"])
    user_id = uuid.UUID(current_user["user_id"])

    # Read file content and check size
    content = await file.read()
    if len(content) > MAX_ATTACHMENT_SIZE:
        raise HTTPException(
            413,
            detail={
                "detail": f"File exceeds maximum size of {MAX_ATTACHMENT_SIZE // (1024 * 1024)} MB",
                "code": "file_too_large",
            },
        )

    # Generate a temporary attachment ID and store the file
    temp_id = str(uuid.uuid4())
    safe_filename = _sanitize_filename(file.filename or "attachment")
    mime_type = file.content_type or "application/octet-stream"

    # Store via storage backend in a path keyed by the temp_id
    storage = get_storage_backend()
    file_path = f"mail_uploads/{temp_id}/{safe_filename}"
    await storage.save(file_path, content)

    return {
        "id": temp_id,
        "filename": safe_filename,
        "mime_type": mime_type,
        "size_bytes": len(content),
        "path": file_path,
    }


# ─── Send (F-MAIL-02) ───


@router.post("/send")
async def send_mail(
    data: MailSendRequest,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("mail:send")),
):
    tenant_id = uuid.UUID(current_user["tenant_id"])
    user_id = uuid.UUID(current_user["user_id"])
    acc_id = _parse_uuid(data.account_id, "account_id")
    account = await _get_account(db, acc_id, tenant_id, user_id)
    await _check_send_permission(db, account, user_id)
    signature = None
    if data.signature_id:
        sig_id = _parse_uuid(data.signature_id, "signature_id")
        signature = (
            await db.execute(
                select(MailSignature).where(
                    and_(MailSignature.id == sig_id, MailSignature.tenant_id == tenant_id)
                )
            )
        ).scalar_one_or_none()
    body_html = data.body_html
    body_text = data.body_text
    subject = data.subject
    if data.template_id:
        tpl_id = _parse_uuid(data.template_id, "template_id")
        template = (
            await db.execute(
                select(MailTemplate).where(
                    and_(MailTemplate.id == tpl_id, MailTemplate.tenant_id == tenant_id)
                )
            )
        ).scalar_one_or_none()
        if template:
            body_html = substitute_template_vars(template.body_html, data.template_vars)
            subject = substitute_template_vars(template.subject, data.template_vars)
    result = await send_mail_via_smtp(
        db,
        tenant_id=tenant_id,
        user_id=user_id,
        account=account,
        to_addrs=data.to,
        cc_addrs=data.cc,
        bcc_addrs=data.bcc,
        subject=subject,
        body_html=body_html,
        body_text=body_text,
        in_reply_to=data.in_reply_to,
        references_header=data.references_header,
        signature=signature,
        attachment_paths=await _resolve_attachment_paths(data.attachments),
    )
    if result.get("status") == "error":
        raise HTTPException(
            500, detail={"detail": result.get("error", "Send failed"), "code": "send_error"}
        )
    return result


# ─── Search (F-MAIL-03) ───


@router.get("/search")
async def search_mails(
    q: str = Query(..., min_length=1),
    account_id: str | None = None,
    page: int = Query(1, ge=1),
    page_size: int = Query(20, ge=1, le=100),
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("mail:read")),
):
    tenant_id = uuid.UUID(current_user["tenant_id"])
    pattern = f"%{q}%"
    stmt = select(Mail).where(
        and_(
            Mail.tenant_id == tenant_id,
            or_(
                Mail.subject.ilike(pattern),
                Mail.body_text.ilike(pattern),
                Mail.from_address.ilike(pattern),
                Mail.to_addresses.ilike(pattern),
            ),
        )
    )
    if account_id:
        a_id = _parse_uuid(account_id, "account_id")
        stmt = stmt.where(Mail.account_id == a_id)
    total = (await db.execute(select(func.count()).select_from(stmt.subquery()))).scalar()
    stmt = stmt.order_by(desc(Mail.received_at)).offset((page - 1) * page_size).limit(page_size)
    mails = (await db.execute(stmt)).scalars().all()
    return {"results": [mail_to_response(m) for m in mails], "total": total}


# ─── Threads (F-MAIL-05) ───


@router.get("/threads")
async def list_threads(
    account_id: str | None = None,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("mail:read")),
):
    tenant_id = uuid.UUID(current_user["tenant_id"])
    stmt = select(Mail).where(Mail.tenant_id == tenant_id)
    if account_id:
        a_id = _parse_uuid(account_id, "account_id")
        stmt = stmt.where(Mail.account_id == a_id)
    mails = (await db.execute(stmt.order_by(desc(Mail.received_at)))).scalars().all()
    threads: dict[str, dict] = {}
    for mail in mails:
        tid = mail.thread_id or mail.message_id
        if tid not in threads:
            threads[tid] = {"thread_id": tid, "subject": mail.subject, "mail_count": 0, "mails": []}
        threads[tid]["mail_count"] += 1
        threads[tid]["mails"].append(mail_to_response(mail))
    return list(threads.values())


# ─── Templates (F-MAIL-06) ───


@router.post("/templates", status_code=201)
async def create_template(
    data: MailTemplateCreate,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("mail:config")),
):
    tenant_id = uuid.UUID(current_user["tenant_id"])
    user_id = uuid.UUID(current_user["user_id"])
    template = MailTemplate(
        tenant_id=tenant_id,
        user_id=user_id,
        name=data.name,
        subject=data.subject,
        body_html=data.body_html,
    )
    db.add(template)
    await db.flush()
    return template_to_response(template)


@router.get("/templates")
async def list_templates(
    db: AsyncSession = Depends(get_db), current_user: dict = Depends(require_permission("mail:read"))
):
    tenant_id = uuid.UUID(current_user["tenant_id"])
    templates = (
        (await db.execute(select(MailTemplate).where(MailTemplate.tenant_id == tenant_id)))
        .scalars()
        .all()
    )
    return [template_to_response(t) for t in templates]


@router.post("/templates/substitute")
async def substitute_template(
    data: TemplateSubstituteRequest,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("mail:read")),
):
    tenant_id = uuid.UUID(current_user["tenant_id"])
    tpl_id = _parse_uuid(data.template_id, "template_id")
    template = (
        await db.execute(
            select(MailTemplate).where(
                and_(MailTemplate.id == tpl_id, MailTemplate.tenant_id == tenant_id)
            )
        )
    ).scalar_one_or_none()
    if not template:
        raise HTTPException(404, detail={"detail": "Template not found", "code": "not_found"})
    return {
        "subject": substitute_template_vars(template.subject, data.variables),
        "body_html": substitute_template_vars(template.body_html, data.variables),
    }


# ─── Signatures (F-MAIL-13) ───


@router.post("/signatures", status_code=201)
async def create_signature(
    data: MailSignatureCreate,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("mail:config")),
):
    tenant_id = uuid.UUID(current_user["tenant_id"])
    user_id = uuid.UUID(current_user["user_id"])
    acc_id = _parse_uuid(data.account_id, "account_id") if data.account_id else None
    sig = MailSignature(
        tenant_id=tenant_id,
        user_id=user_id,
        account_id=acc_id,
        name=data.name,
        body_html=data.body_html,
        is_default=data.is_default,
    )
    db.add(sig)
    await db.flush()
    return signature_to_response(sig)


@router.get("/signatures")
async def list_signatures(
    db: AsyncSession = Depends(get_db), current_user: dict = Depends(require_permission("mail:read"))
):
    tenant_id = uuid.UUID(current_user["tenant_id"])
    sigs = (
        (await db.execute(select(MailSignature).where(MailSignature.tenant_id == tenant_id)))
        .scalars()
        .all()
    )
    return [signature_to_response(s) for s in sigs]


# ─── Rules (F-MAIL-07) ───


@router.post("/rules", status_code=201)
async def create_rule(
    data: MailRuleCreate,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("mail:config")),
):
    tenant_id = uuid.UUID(current_user["tenant_id"])
    acc_id = _parse_uuid(data.account_id, "account_id") if data.account_id else None
    rule = MailRule(
        tenant_id=tenant_id,
        account_id=acc_id,
        name=data.name,
        priority=data.priority,
        is_active=data.is_active,
        conditions=json.dumps(data.conditions),
        actions=json.dumps(data.actions),
    )
    db.add(rule)
    await db.flush()
    return rule_to_response(rule)


@router.get("/rules")
async def list_rules(
    db: AsyncSession = Depends(get_db), current_user: dict = Depends(require_permission("mail:read"))
):
    tenant_id = uuid.UUID(current_user["tenant_id"])
    rules = (
        (
            await db.execute(
                select(MailRule).where(MailRule.tenant_id == tenant_id).order_by(MailRule.priority)
            )
        )
        .scalars()
        .all()
    )
    return [rule_to_response(r) for r in rules]


@router.delete("/rules/{rule_id}", status_code=204)
async def delete_rule(
    rule_id: str, db: AsyncSession = Depends(get_db), current_user: dict = Depends(require_permission("mail:config"))
):
    tenant_id = uuid.UUID(current_user["tenant_id"])
    r_id = _parse_uuid(rule_id, "rule_id")
    rule = (
        await db.execute(
            select(MailRule).where(and_(MailRule.id == r_id, MailRule.tenant_id == tenant_id))
        )
    ).scalar_one_or_none()
    if not rule:
        raise HTTPException(404, detail={"detail": "Rule not found", "code": "not_found"})
    await db.delete(rule)


# ─── Vacation (F-MAIL-08) ───


@router.post("/vacation")
async def configure_vacation(
    data: VacationConfig,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("mail:config")),
):
    tenant_id = uuid.UUID(current_user["tenant_id"])
    user_id = uuid.UUID(current_user["user_id"])
    acc_id = _parse_uuid(data.account_id, "account_id")
    await _get_account(db, acc_id, tenant_id, user_id)
    vacation_data = {
        "is_enabled": data.is_enabled,
        "subject": data.subject,
        "body_text": data.body_text,
        "body_html": data.body_html,
    }
    test_sender = "test@example.com"
    should_send = await should_send_vacation_reply(db, acc_id, test_sender, tenant_id)
    if should_send:
        await log_vacation_sent(db, acc_id, test_sender, tenant_id)
    should_send_2 = await should_send_vacation_reply(db, acc_id, test_sender, tenant_id)
    return {
        "configured": True,
        "vacation": vacation_data,
        "dedup_test": {"first_send": should_send, "second_send_blocked": not should_send_2},
    }


@router.post("/vacation/test-dedup")
async def test_vacation_dedup(
    account_id: str = Query(...),
    sender: str = Query(...),
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("mail:config")),
):
    tenant_id = uuid.UUID(current_user["tenant_id"])
    user_id = uuid.UUID(current_user["user_id"])
    acc_id = _parse_uuid(account_id, "account_id")
    await _get_account(db, acc_id, tenant_id, user_id)
    should_send_1 = await should_send_vacation_reply(db, acc_id, sender, tenant_id)
    if should_send_1:
        await log_vacation_sent(db, acc_id, sender, tenant_id)
    should_send_2 = await should_send_vacation_reply(db, acc_id, sender, tenant_id)
    return {
        "first_should_send": should_send_1,
        "second_should_send": should_send_2,
        "dedup_works": should_send_1 and not should_send_2,
    }


# ─── PGP (F-MAIL-12) ───


@router.post("/pgp/keys", status_code=201)
async def import_pgp_key(
    data: PgpKeyImport,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("mail:config")),
):
    tenant_id = uuid.UUID(current_user["tenant_id"])
    user_id = uuid.UUID(current_user["user_id"])
    key_id, public_key_armored = import_pgp_private_key(data.private_key_armored, data.passphrase)
    encrypted_private = encrypt_password(data.private_key_armored)
    pgp_key = PgpKey(
        tenant_id=tenant_id,
        user_id=user_id,
        key_id=key_id,
        encrypted_private_key=encrypted_private,
        public_key_armored=public_key_armored,
    )
    db.add(pgp_key)
    await db.flush()
    return {"id": str(pgp_key.id), "key_id": key_id, "public_key_armored": public_key_armored}


@router.get("/pgp/keys")
async def list_pgp_keys(
    db: AsyncSession = Depends(get_db), current_user: dict = Depends(require_permission("mail:read"))
):
    tenant_id = uuid.UUID(current_user["tenant_id"])
    user_id = uuid.UUID(current_user["user_id"])
    keys = (
        (
            await db.execute(
                select(PgpKey).where(and_(PgpKey.tenant_id == tenant_id, PgpKey.user_id == user_id))
            )
        )
        .scalars()
        .all()
    )
    return [
        {"id": str(k.id), "key_id": k.key_id, "public_key_armored": k.public_key_armored}
        for k in keys
    ]


@router.post("/pgp/encrypt")
async def encrypt_message(
    data: dict = Body(...),
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("mail:send")),
):
    tenant_id = uuid.UUID(current_user["tenant_id"])
    contact_id = (
        _parse_uuid(data.get("contact_id", ""), "contact_id") if data.get("contact_id") else None
    )
    plaintext = data.get("plaintext", "")
    if not contact_id:
        raise HTTPException(400, detail={"detail": "contact_id required", "code": "bad_request"})
    contact_key = (
        await db.execute(
            select(ContactPgpKey).where(
                and_(ContactPgpKey.contact_id == contact_id, ContactPgpKey.tenant_id == tenant_id)
            )
        )
    ).scalar_one_or_none()
    if not contact_key:
        raise HTTPException(404, detail={"detail": "No PGP key for contact", "code": "not_found"})
    return {"encrypted": pgp_encrypt_message(plaintext, contact_key.public_key_armored)}


# ─── Labels (F-MAIL-09) ───


@router.post("/labels", status_code=201)
async def create_label(
    data: MailLabelCreate,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("mail:config")),
):
    tenant_id = uuid.UUID(current_user["tenant_id"])
    user_id = uuid.UUID(current_user["user_id"])
    label = MailLabel(tenant_id=tenant_id, name=data.name, color=data.color, user_id=user_id)
    db.add(label)
    await db.flush()
    return label_to_response(label)


@router.get("/labels")
async def list_labels(
    db: AsyncSession = Depends(get_db), current_user: dict = Depends(require_permission("mail:read"))
):
    tenant_id = uuid.UUID(current_user["tenant_id"])
    labels = (
        (await db.execute(select(MailLabel).where(MailLabel.tenant_id == tenant_id)))
        .scalars()
        .all()
    )
    return [label_to_response(lbl) for lbl in labels]


# ─── Contact PGP Keys (F-MAIL-12) ───


@router.post("/contacts/{contact_id}/pgp-key", status_code=201)
async def store_contact_pgp_key(
    contact_id: str,
    data: dict = Body(...),
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("mail:config")),
):
    tenant_id = uuid.UUID(current_user["tenant_id"])
    c_id = _parse_uuid(contact_id, "contact_id")
    public_key_armored = data.get("public_key_armored", "")
    if not public_key_armored:
        raise HTTPException(
            400, detail={"detail": "public_key_armored required", "code": "bad_request"}
        )
    key_id = import_pgp_public_key(public_key_armored)
    existing = (
        await db.execute(
            select(ContactPgpKey).where(
                and_(ContactPgpKey.contact_id == c_id, ContactPgpKey.tenant_id == tenant_id)
            )
        )
    ).scalar_one_or_none()
    if existing:
        existing.public_key_armored = public_key_armored
        existing.key_id = key_id
        await db.flush()
        return {"id": str(existing.id), "contact_id": str(c_id), "key_id": key_id}
    contact_key = ContactPgpKey(
        tenant_id=tenant_id, contact_id=c_id, public_key_armored=public_key_armored, key_id=key_id
    )
    db.add(contact_key)
    await db.flush()
    return {"id": str(contact_key.id), "contact_id": str(c_id), "key_id": key_id}


# ─── Rule Engine (F-MAIL-07) ───


@router.post("/{mail_id}/apply-rules")
async def apply_rules(
    mail_id: str, db: AsyncSession = Depends(get_db), current_user: dict = Depends(require_permission("mail:config"))
):
    tenant_id = uuid.UUID(current_user["tenant_id"])
    m_id = _parse_uuid(mail_id, "mail_id")
    mail = (
        await db.execute(select(Mail).where(and_(Mail.id == m_id, Mail.tenant_id == tenant_id)))
    ).scalar_one_or_none()
    if not mail:
        raise HTTPException(404, detail={"detail": "Mail not found", "code": "not_found"})
    applied = await apply_rules_to_mail(db, mail, tenant_id)
    return {"applied_rules": applied}


# ─── Reply / Forward / Flags / Attachments / Link / Create-Event / Labels ───


@router.post("/{mail_id}/reply")
async def reply_mail(
    mail_id: str,
    data: MailReplyRequest,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("mail:send")),
):
    tenant_id = uuid.UUID(current_user["tenant_id"])
    user_id = uuid.UUID(current_user["user_id"])
    m_id = _parse_uuid(mail_id, "mail_id")
    original = (
        await db.execute(select(Mail).where(and_(Mail.id == m_id, Mail.tenant_id == tenant_id)))
    ).scalar_one_or_none()
    if not original:
        raise HTTPException(404, detail={"detail": "Mail not found", "code": "not_found"})
    account = await _get_account(db, original.account_id, tenant_id, user_id)
    await _check_delegate_access(db, account, user_id, "read")
    await _check_send_permission(db, account, user_id)
    signature = None
    if data.signature_id:
        sig_id = _parse_uuid(data.signature_id, "signature_id")
        signature = (
            await db.execute(
                select(MailSignature).where(
                    and_(MailSignature.id == sig_id, MailSignature.tenant_id == tenant_id)
                )
            )
        ).scalar_one_or_none()
    result = await reply_to_mail(
        db,
        tenant_id=tenant_id,
        user_id=user_id,
        original_mail=original,
        account=account,
        body_html=data.body_html,
        body_text=data.body_text,
        reply_to_all=data.reply_to_all,
        signature=signature,
    )
    if result.get("status") == "error":
        raise HTTPException(
            500, detail={"detail": result.get("error", "Reply failed"), "code": "send_error"}
        )
    return result


@router.post("/{mail_id}/forward")
async def forward_mail_endpoint(
    mail_id: str,
    data: MailForwardRequest,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("mail:send")),
):
    tenant_id = uuid.UUID(current_user["tenant_id"])
    user_id = uuid.UUID(current_user["user_id"])
    m_id = _parse_uuid(mail_id, "mail_id")
    original = (
        await db.execute(select(Mail).where(and_(Mail.id == m_id, Mail.tenant_id == tenant_id)))
    ).scalar_one_or_none()
    if not original:
        raise HTTPException(404, detail={"detail": "Mail not found", "code": "not_found"})
    account = await _get_account(db, original.account_id, tenant_id, user_id)
    await _check_delegate_access(db, account, user_id, "read")
    await _check_send_permission(db, account, user_id)
    signature = None
    if data.signature_id:
        sig_id = _parse_uuid(data.signature_id, "signature_id")
        signature = (
            await db.execute(
                select(MailSignature).where(
                    and_(MailSignature.id == sig_id, MailSignature.tenant_id == tenant_id)
                )
            )
        ).scalar_one_or_none()
    result = await forward_mail(
        db,
        tenant_id=tenant_id,
        user_id=user_id,
        original_mail=original,
        account=account,
        to_addrs=data.to,
        cc_addrs=data.cc,
        body_html=data.body_html,
        body_text=data.body_text,
        signature=signature,
    )
    if result.get("status") == "error":
        raise HTTPException(
            500, detail={"detail": result.get("error", "Forward failed"), "code": "send_error"}
        )
    return result


@router.patch("/{mail_id}/flags")
async def update_flags(
    mail_id: str,
    data: MailFlagsUpdate,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("mail:write")),
):
    tenant_id = uuid.UUID(current_user["tenant_id"])
    user_id = uuid.UUID(current_user["user_id"])
    m_id = _parse_uuid(mail_id, "mail_id")
    mail = (
        await db.execute(select(Mail).where(and_(Mail.id == m_id, Mail.tenant_id == tenant_id)))
    ).scalar_one_or_none()
    if not mail:
        raise HTTPException(404, detail={"detail": "Mail not found", "code": "not_found"})
    account = await _get_account(db, mail.account_id, tenant_id, user_id)
    await _check_delegate_access(db, account, user_id, "write")
    if data.is_seen is not None:
        mail.is_seen = data.is_seen
    if data.is_flagged is not None:
        mail.is_flagged = data.is_flagged
    if data.flag_type is not None:
        mail.flag_type = data.flag_type if data.flag_type else None
    if data.is_draft is not None:
        mail.is_draft = data.is_draft
    if data.is_answered is not None:
        mail.is_answered = data.is_answered
    if data.is_forwarded is not None:
        mail.is_forwarded = data.is_forwarded
    await db.flush()
    # Sync flags to IMAP server (non-critical — failures are logged but don't break the API)
    try:
        await mail_services.imap_sync_mail_flags(db, m_id, tenant_id)
    except Exception as exc:
        import logging
        logging.getLogger(__name__).warning("IMAP flag sync failed for mail %s: %s", m_id, exc)
    return mail_to_response(mail)


@router.get("/{mail_id}/attachments/{att_id}")
async def download_attachment(
    mail_id: str,
    att_id: str,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("mail:read")),
):
    tenant_id = uuid.UUID(current_user["tenant_id"])
    user_id = uuid.UUID(current_user["user_id"])
    m_id = _parse_uuid(mail_id, "mail_id")
    a_id = _parse_uuid(att_id, "att_id")
    mail = (
        await db.execute(select(Mail).where(and_(Mail.id == m_id, Mail.tenant_id == tenant_id)))
    ).scalar_one_or_none()
    if not mail:
        raise HTTPException(404, detail={"detail": "Mail not found", "code": "not_found"})
    account = await _get_account(db, mail.account_id, tenant_id, user_id)
    await _check_delegate_access(db, account, user_id, "read")
    attachment = (
        await db.execute(
            select(MailAttachment).where(
                and_(MailAttachment.id == a_id, MailAttachment.mail_id == m_id)
            )
        )
    ).scalar_one_or_none()
    if not attachment:
        raise HTTPException(404, detail={"detail": "Attachment not found", "code": "not_found"})
    storage = get_storage_backend()
    if not await storage.exists(attachment.storage_path):
        raise HTTPException(
            404, detail={"detail": "File not found on disk", "code": "file_missing"}
        )

    content = await storage.read(attachment.storage_path)

    async def file_stream():
        yield content

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


@router.post("/{mail_id}/link")
async def link_mail(
    mail_id: str,
    data: MailLinkRequest,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("mail:write")),
):
    tenant_id = uuid.UUID(current_user["tenant_id"])
    user_id = uuid.UUID(current_user["user_id"])
    m_id = _parse_uuid(mail_id, "mail_id")
    mail = (
        await db.execute(select(Mail).where(and_(Mail.id == m_id, Mail.tenant_id == tenant_id)))
    ).scalar_one_or_none()
    if not mail:
        raise HTTPException(404, detail={"detail": "Mail not found", "code": "not_found"})
    account = await _get_account(db, mail.account_id, tenant_id, user_id)
    await _check_delegate_access(db, account, user_id, "write")
    if data.contact_id:
        mail.contact_id = _parse_uuid(data.contact_id, "contact_id")
    await db.flush()
    return {
        "linked": True,
        "contact_id": str(mail.contact_id) if mail.contact_id else None,
    }


@router.post("/{mail_id}/create-event")
async def create_event_from_mail(
    mail_id: str,
    data: MailCreateEventRequest,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("mail:write")),
):
    tenant_id = uuid.UUID(current_user["tenant_id"])
    user_id = uuid.UUID(current_user["user_id"])
    m_id = _parse_uuid(mail_id, "mail_id")
    mail = (
        await db.execute(select(Mail).where(and_(Mail.id == m_id, Mail.tenant_id == tenant_id)))
    ).scalar_one_or_none()
    if not mail:
        raise HTTPException(404, detail={"detail": "Mail not found", "code": "not_found"})
    account = await _get_account(db, mail.account_id, tenant_id, user_id)
    await _check_delegate_access(db, account, user_id, "write")
    try:
        from app.plugins.builtins.calendar.models import Calendar, CalendarEntry
    except ImportError:
        return {"created": False, "error": "Calendar plugin not available"}
    cal_id = _parse_uuid(data.calendar_id, "calendar_id")
    calendar = (
        await db.execute(
            select(Calendar).where(and_(Calendar.id == cal_id, Calendar.tenant_id == tenant_id))
        )
    ).scalar_one_or_none()
    if not calendar:
        raise HTTPException(404, detail={"detail": "Calendar not found", "code": "not_found"})
    title = data.title or mail.subject
    description = data.description or (mail.body_text[:500] if mail.body_text else "")
    entry = CalendarEntry(
        tenant_id=tenant_id,
        calendar_id=cal_id,
        title=title,
        description=description,
        start_datetime=datetime.fromisoformat(data.start) if data.start else datetime.now(UTC),
        end_datetime=datetime.fromisoformat(data.end) if data.end else datetime.now(UTC),
        created_by=user_id,
        is_all_day=False,
    )
    db.add(entry)
    await db.flush()
    return {"created": True, "event_id": str(entry.id)}


@router.post("/{mail_id}/labels")
async def assign_label(
    mail_id: str,
    data: MailLabelAssign,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("mail:write")),
):
    tenant_id = uuid.UUID(current_user["tenant_id"])
    user_id = uuid.UUID(current_user["user_id"])
    m_id = _parse_uuid(mail_id, "mail_id")
    l_id = _parse_uuid(data.label_id, "label_id")
    mail = (
        await db.execute(select(Mail).where(and_(Mail.id == m_id, Mail.tenant_id == tenant_id)))
    ).scalar_one_or_none()
    if not mail:
        raise HTTPException(404, detail={"detail": "Mail not found", "code": "not_found"})
    account = await _get_account(db, mail.account_id, tenant_id, user_id)
    await _check_delegate_access(db, account, user_id, "write")
    label = (
        await db.execute(
            select(MailLabel).where(and_(MailLabel.id == l_id, MailLabel.tenant_id == tenant_id))
        )
    ).scalar_one_or_none()
    if not label:
        raise HTTPException(404, detail={"detail": "Label not found", "code": "not_found"})
    existing = (
        await db.execute(
            select(MailLabelAssignment).where(
                and_(MailLabelAssignment.mail_id == m_id, MailLabelAssignment.label_id == l_id)
            )
        )
    ).scalar_one_or_none()
    if existing:
        return {"assigned": True, "already_assigned": True}
    db.add(MailLabelAssignment(tenant_id=tenant_id, mail_id=m_id, label_id=l_id))
    await db.flush()
    return {"assigned": True, "label_id": str(l_id)}


# ─── Drafts (F-MAIL-15) ───


@router.post("/drafts", status_code=201)
async def create_draft(
    data: MailDraftCreate,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("mail:write")),
):
    tenant_id = uuid.UUID(current_user["tenant_id"])
    user_id = uuid.UUID(current_user["user_id"])
    acc_id = _parse_uuid(data.account_id, "account_id")
    account = await _get_account(db, acc_id, tenant_id, user_id)
    await _check_delegate_access(db, account, user_id, "write")
    mail = await save_draft(db, acc_id, tenant_id, user_id, data.model_dump())
    return mail_to_response(mail)


@router.put("/drafts/{mail_id}")
async def update_draft_route(
    mail_id: str,
    data: MailDraftCreate,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("mail:write")),
):
    tenant_id = uuid.UUID(current_user["tenant_id"])
    user_id = uuid.UUID(current_user["user_id"])
    m_id = _parse_uuid(mail_id, "mail_id")
    mail = (
        await db.execute(select(Mail).where(and_(Mail.id == m_id, Mail.tenant_id == tenant_id)))
    ).scalar_one_or_none()
    if not mail:
        raise HTTPException(404, detail={"detail": "Mail not found", "code": "not_found"})
    account = await _get_account(db, mail.account_id, tenant_id, user_id)
    await _check_delegate_access(db, account, user_id, "write")
    mail = await update_draft(db, m_id, tenant_id, data.model_dump())
    return mail_to_response(mail)


# ─── Delete / Move (dynamic /{mail_id} routes) ───


@router.delete("/{mail_id}", status_code=204)
async def delete_mail(
    mail_id: str,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("mail:delete")),
):
    tenant_id = uuid.UUID(current_user["tenant_id"])
    user_id = uuid.UUID(current_user["user_id"])
    m_id = _parse_uuid(mail_id, "mail_id")
    mail = (
        await db.execute(select(Mail).where(and_(Mail.id == m_id, Mail.tenant_id == tenant_id)))
    ).scalar_one_or_none()
    if not mail:
        raise HTTPException(404, detail={"detail": "Mail not found", "code": "not_found"})
    account = await _get_account(db, mail.account_id, tenant_id, user_id)
    await _check_delegate_access(db, account, user_id, "delete")

    # Find the Trash folder for this account
    trash_folder = None
    all_folders = (
        await db.execute(
            select(MailFolder).where(
                and_(MailFolder.account_id == mail.account_id, MailFolder.tenant_id == tenant_id)
            )
        )
    ).scalars().all()
    for f in all_folders:
        if 'trash' in f.imap_name.lower() or 'trash' in f.name.lower() or 'papierkorb' in f.name.lower():
            trash_folder = f
            break

    if trash_folder and mail.folder_id == trash_folder.id:
        # Mail is already in Trash → permanent delete from DB + IMAP EXPUNGE
        # IMPORTANT: Call imap_delete_mail BEFORE db.delete, because imap_delete_mail
        # reads the mail from DB to find its folder and imap_uid.
        try:
            await mail_services.imap_delete_mail(db, m_id, tenant_id, permanent=True)
            await db.delete(mail)
            await db.flush()
        except Exception as exc:
            import logging
            logging.getLogger(__name__).warning("IMAP permanent delete failed for mail %s: %s, queuing for retry", m_id, exc)
            await db.delete(mail)
            await db.flush()
            queue_entry = MailSyncQueue(
                tenant_id=tenant_id,
                mail_id=m_id,
                operation="delete",
                payload={"permanent": True},
                status="pending",
                last_error=str(exc),
            )
            db.add(queue_entry)
            await db.flush()
    elif trash_folder:
        # Mail is NOT in Trash → move to Trash folder (standard mail program logic)
        # IMPORTANT: Call imap_delete_mail BEFORE changing folder_id,
        # because imap_delete_mail reads mail.folder_id to find the IMAP folder.
        try:
            await mail_services.imap_delete_mail(db, m_id, tenant_id, permanent=False)
            mail.folder_id = trash_folder.id
            await db.flush()
        except Exception as exc:
            import logging
            logging.getLogger(__name__).warning("IMAP move to Trash failed for mail %s: %s, queuing for retry", m_id, exc)
            mail.folder_id = trash_folder.id
            await db.flush()
            queue_entry = MailSyncQueue(
                tenant_id=tenant_id,
                mail_id=m_id,
                operation="delete",
                payload={"permanent": False},
                status="pending",
                last_error=str(exc),
            )
            db.add(queue_entry)
            await db.flush()
    else:
        # No Trash folder found → permanent delete from DB + IMAP EXPUNGE
        # IMPORTANT: Call imap_delete_mail BEFORE db.delete, because imap_delete_mail
        # reads the mail from DB to find its folder and imap_uid.
        try:
            await mail_services.imap_delete_mail(db, m_id, tenant_id, permanent=True)
            await db.delete(mail)
            await db.flush()
        except Exception as exc:
            import logging
            logging.getLogger(__name__).warning("IMAP delete failed for mail %s: %s, queuing for retry", m_id, exc)
            await db.delete(mail)
            await db.flush()
            queue_entry = MailSyncQueue(
                tenant_id=tenant_id,
                mail_id=m_id,
                operation="delete",
                payload={"permanent": True},
                status="pending",
                last_error=str(exc),
            )
            db.add(queue_entry)
            await db.flush()


@router.post("/{mail_id}/move")
async def move_mail(
    mail_id: str,
    data: MailMoveRequest,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("mail:write")),
):
    tenant_id = uuid.UUID(current_user["tenant_id"])
    user_id = uuid.UUID(current_user["user_id"])
    m_id = _parse_uuid(mail_id, "mail_id")
    target_f_id = _parse_uuid(data.target_folder_id, "target_folder_id")
    mail = (
        await db.execute(select(Mail).where(and_(Mail.id == m_id, Mail.tenant_id == tenant_id)))
    ).scalar_one_or_none()
    if not mail:
        raise HTTPException(404, detail={"detail": "Mail not found", "code": "not_found"})
    account = await _get_account(db, mail.account_id, tenant_id, user_id)
    await _check_delegate_access(db, account, user_id, "write")
    # Verify target folder exists and belongs to same tenant
    target_folder = (
        await db.execute(
            select(MailFolder).where(
                and_(MailFolder.id == target_f_id, MailFolder.tenant_id == tenant_id)
            )
        )
    ).scalar_one_or_none()
    if not target_folder:
        raise HTTPException(404, detail={"detail": "Target folder not found", "code": "not_found"})
    # Update mail.folder_id
    mail.folder_id = target_f_id
    await db.flush()
    # IMAP move: queue for retry if it fails
    try:
        await mail_services.imap_move_mail(db, m_id, target_f_id, tenant_id)
    except Exception as exc:
        import logging
        logging.getLogger(__name__).warning("IMAP move failed for mail %s: %s, queuing for retry", m_id, exc)
        queue_entry = MailSyncQueue(
            tenant_id=tenant_id,
            mail_id=m_id,
            operation="move",
            payload={"target_folder_id": str(target_f_id)},
            status="pending",
            last_error=str(exc),
        )
        db.add(queue_entry)
        await db.flush()
    return mail_to_response(mail)


# ═══════════════════════════════════════════════════════════════
# DYNAMIC ROUTES — registered LAST so static routes are not shadowed
# ═══════════════════════════════════════════════════════════════


@router.get("")
async def list_mails(
    folder_id: str | None = None,
    account_id: str | None = None,
    page: int = Query(1, ge=1),
    page_size: int = Query(20, ge=1, le=100),
    sort_by: str = Query("date", pattern="^(date|from|subject)$"),
    sort_order: str = Query("desc", pattern="^(asc|desc)$"),
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_permission("mail:read")),
):
    tenant_id = uuid.UUID(current_user["tenant_id"])
    stmt = select(Mail).where(Mail.tenant_id == tenant_id)
    if folder_id:
        f_id = _parse_uuid(folder_id, "folder_id")
        stmt = stmt.where(Mail.folder_id == f_id)
    if account_id:
        a_id = _parse_uuid(account_id, "account_id")
        stmt = stmt.where(Mail.account_id == a_id)
    total = (await db.execute(select(func.count()).select_from(stmt.subquery()))).scalar()
    # Dynamic sorting
    sort_columns = {
        "date": Mail.received_at,
        "from": Mail.from_address,
        "subject": Mail.subject,
    }
    sort_col = sort_columns.get(sort_by, Mail.received_at)
    order_func = desc if sort_order == "desc" else asc
    stmt = stmt.order_by(order_func(sort_col)).offset((page - 1) * page_size).limit(page_size)
    mails = (await db.execute(stmt)).scalars().all()
    return {
        "mails": [mail_to_response(m) for m in mails],
        "total": total,
        "page": page,
        "page_size": page_size,
    }


@router.get("/{mail_id}")
async def get_mail(
    mail_id: str, db: AsyncSession = Depends(get_db), current_user: dict = Depends(require_permission("mail:read"))
):
    tenant_id = uuid.UUID(current_user["tenant_id"])
    user_id = uuid.UUID(current_user["user_id"])
    m_id = _parse_uuid(mail_id, "mail_id")
    mail = (
        await db.execute(select(Mail).where(and_(Mail.id == m_id, Mail.tenant_id == tenant_id)))
    ).scalar_one_or_none()
    if not mail:
        raise HTTPException(404, detail={"detail": "Mail not found", "code": "not_found"})
    account = await _get_account(db, mail.account_id, tenant_id, user_id)
    await _check_delegate_access(db, account, user_id, "read")
    attachments = (
        (await db.execute(select(MailAttachment).where(MailAttachment.mail_id == mail.id)))
        .scalars()
        .all()
    )
    label_assignments = (
        (
            await db.execute(
                select(MailLabel)
                .join(MailLabelAssignment, MailLabelAssignment.label_id == MailLabel.id)
                .where(MailLabelAssignment.mail_id == mail.id)
            )
        )
        .scalars()
        .all()
    )
    return mail_to_response(mail, attachments=list(attachments), labels=list(label_assignments))
