"""Calendar plugin routes — calendars, entries, links, subtasks, bulk, kanban, CSV, ICS, resources."""

from __future__ import annotations

import csv
import io
import secrets
import uuid
from datetime import UTC, datetime, timedelta

from fastapi import (
    APIRouter,
    Depends,
    File,
    HTTPException,
    Response,
    UploadFile,
    status,
)
from fastapi.responses import StreamingResponse
from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.db import get_db
from app.deps import get_current_user, require_admin, require_permission
from app.plugins.builtins.calendar.ics_utils import (
    export_entries_to_ics,
    ics_events_to_entry_data,
    parse_ics,
)
from app.plugins.builtins.calendar.models import (
    Calendar,
    CalendarEntry,
    CalendarEntryLink,
    CalendarShare,
    Resource,
    ResourceBooking,
    Subtask,
)
from app.plugins.builtins.calendar.recurrence import generate_occurrences
from app.plugins.builtins.calendar.schemas import (
    BookResourceRequest,
    BulkAction,
    CalendarCreate,
    CalendarUpdate,
    EntryCreate,
    EntryUpdate,
    LinkRequest,
    ResourceCreate,
    ShareRequest,
    SubtaskCreate,
    SubtaskUpdate,
)

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

calendar_router = APIRouter(prefix="/api/v1/calendars", tags=["calendar"])
resource_router = APIRouter(prefix="/api/v1/resources", tags=["calendar"])


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 _entry_to_dict(
    entry: CalendarEntry, links: list | None = None, subtasks: list | None = None
) -> dict:
    return {
        "id": str(entry.id),
        "calendar_id": str(entry.calendar_id),
        "entry_type": entry.entry_type,
        "subtype": entry.subtype,
        "title": entry.title,
        "description": entry.description,
        "start_at": entry.start_at.isoformat() if entry.start_at else None,
        "end_at": entry.end_at.isoformat() if entry.end_at else None,
        "all_day": entry.all_day,
        "location": entry.location,
        "due_date": entry.due_date.isoformat() if entry.due_date else None,
        "priority": entry.priority,
        "status": entry.status,
        "assigned_to": str(entry.assigned_to) if entry.assigned_to else None,
        "reminder": entry.reminder,
        "recurrence": entry.recurrence,
        "created_by": str(entry.created_by),
        "created_at": entry.created_at.isoformat() if entry.created_at else None,
        "updated_at": entry.updated_at.isoformat() if entry.updated_at else None,
        "links": links or [],
        "subtasks": subtasks or [],
    }


def _calendar_to_dict(cal: Calendar) -> dict:
    return {
        "id": str(cal.id),
        "name": cal.name,
        "color": cal.color,
        "type": cal.type,
        "owner_id": str(cal.owner_id),
        "ics_token": cal.ics_token,
        "created_at": cal.created_at.isoformat() if cal.created_at else None,
        "updated_at": cal.updated_at.isoformat() if cal.updated_at else None,
    }


async def _get_calendar_or_404(
    db: AsyncSession, calendar_id: uuid.UUID, tenant_id: uuid.UUID
) -> Calendar:
    result = await db.execute(
        select(Calendar).where(
            Calendar.id == calendar_id,
            Calendar.tenant_id == tenant_id,
            Calendar.deleted_at.is_(None),
        )
    )
    cal = result.scalar_one_or_none()
    if cal is None:
        raise HTTPException(404, detail={"detail": "Calendar not found", "code": "not_found"})
    return cal


async def _get_entry_or_404(
    db: AsyncSession, entry_id: uuid.UUID, tenant_id: uuid.UUID
) -> CalendarEntry:
    result = await db.execute(
        select(CalendarEntry).where(
            CalendarEntry.id == entry_id,
            CalendarEntry.tenant_id == tenant_id,
            CalendarEntry.deleted_at.is_(None),
        )
    )
    entry = result.scalar_one_or_none()
    if entry is None:
        raise HTTPException(404, detail={"detail": "Entry not found", "code": "not_found"})
    return entry


async def _check_write_permission(
    db: AsyncSession, calendar_id: uuid.UUID, user_id: uuid.UUID, role: str
) -> bool:
    """Check if user has write permission on calendar (owner, admin, or write share)."""
    if role == "admin":
        return True
    result = await db.execute(select(Calendar).where(Calendar.id == calendar_id))
    cal = result.scalar_one_or_none()
    if cal and cal.owner_id == user_id:
        return True
    result = await db.execute(
        select(CalendarShare).where(
            CalendarShare.calendar_id == calendar_id,
            CalendarShare.user_id == user_id,
            CalendarShare.permission == "write",
        )
    )
    return result.scalar_one_or_none() is not None


# ─── Calendar CRUD ───


@calendar_router.get("", dependencies=[Depends(require_permission("calendar:read"))])
async def list_calendars(
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(get_current_user),
):
    """AC1: GET /api/v1/calendars → 200 + calendar list."""
    tenant_id = uuid.UUID(current_user["tenant_id"])
    result = await db.execute(
        select(Calendar).where(
            Calendar.tenant_id == tenant_id,
            Calendar.deleted_at.is_(None),
        )
    )
    cals = result.scalars().all()
    return [_calendar_to_dict(c) for c in cals]


@calendar_router.post("", status_code=status.HTTP_201_CREATED, dependencies=[Depends(require_permission("calendar:write"))])
async def create_calendar(
    body: CalendarCreate,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(get_current_user),
):
    """AC2: POST /api/v1/calendars → 201, calendar created."""
    tenant_id = uuid.UUID(current_user["tenant_id"])
    user_id = uuid.UUID(current_user["user_id"])
    cal = Calendar(
        tenant_id=tenant_id,
        name=body.name,
        color=body.color,
        type=body.type,
        owner_id=user_id,
    )
    db.add(cal)
    await db.flush()
    return _calendar_to_dict(cal)


@calendar_router.patch("/{calendar_id}", dependencies=[Depends(require_permission("calendar:write"))])
async def update_calendar(
    calendar_id: str,
    body: CalendarUpdate,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(get_current_user),
):
    """AC3: PATCH /api/v1/calendars/{id} → 200, updated."""
    tenant_id = uuid.UUID(current_user["tenant_id"])
    user_id = uuid.UUID(current_user["user_id"])
    cal_id = _parse_uuid(calendar_id, "calendar_id")
    cal = await _get_calendar_or_404(db, cal_id, tenant_id)
    if cal.owner_id != user_id and current_user.get("role") != "admin":
        raise HTTPException(
            403, detail={"detail": "Only owner or admin can update", "code": "forbidden"}
        )
    if body.name is not None:
        cal.name = body.name
    if body.color is not None:
        cal.color = body.color
    await db.flush()
    await db.refresh(cal)
    return _calendar_to_dict(cal)


@calendar_router.delete("/{calendar_id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(require_permission("calendar:delete"))])
async def delete_calendar(
    calendar_id: str,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(get_current_user),
):
    """AC4: DELETE /api/v1/calendars/{id} → 204, cascade delete entries + shares."""
    tenant_id = uuid.UUID(current_user["tenant_id"])
    user_id = uuid.UUID(current_user["user_id"])
    cal_id = _parse_uuid(calendar_id, "calendar_id")
    cal = await _get_calendar_or_404(db, cal_id, tenant_id)
    if cal.owner_id != user_id and current_user.get("role") != "admin":
        raise HTTPException(
            403, detail={"detail": "Only owner or admin can delete", "code": "forbidden"}
        )
    # Soft delete calendar
    cal.deleted_at = datetime.now(UTC)
    # Soft delete entries
    await db.execute(
        update(CalendarEntry)
        .where(CalendarEntry.calendar_id == cal_id, CalendarEntry.deleted_at.is_(None))
        .values(deleted_at=datetime.now(UTC))
    )
    # Delete shares
    await db.execute(
        update(CalendarShare)
        .where(CalendarShare.calendar_id == cal_id)
        .values(permission="read")  # Mark as inactive — actual cleanup via cascade
    )
    await db.flush()
    return Response(status_code=204)


@calendar_router.post("/{calendar_id}/share", dependencies=[Depends(require_permission("calendar:share"))])
async def share_calendar(
    calendar_id: str,
    body: ShareRequest,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(get_current_user),
):
    """AC5: POST /api/v1/calendars/{id}/share → 200, calendar shared."""
    tenant_id = uuid.UUID(current_user["tenant_id"])
    user_id = uuid.UUID(current_user["user_id"])
    cal_id = _parse_uuid(calendar_id, "calendar_id")
    cal = await _get_calendar_or_404(db, cal_id, tenant_id)
    if cal.owner_id != user_id and current_user.get("role") != "admin":
        raise HTTPException(
            403, detail={"detail": "Only owner or admin can share", "code": "forbidden"}
        )
    if not body.user_id and not body.group_id:
        raise HTTPException(
            400, detail={"detail": "Must specify user_id or group_id", "code": "missing_target"}
        )
    share = CalendarShare(
        tenant_id=tenant_id,
        calendar_id=cal_id,
        user_id=_parse_uuid(body.user_id, "user_id") if body.user_id else None,
        group_id=_parse_uuid(body.group_id, "group_id") if body.group_id else None,
        permission=body.permission,
    )
    db.add(share)
    await db.flush()
    return {
        "id": str(share.id),
        "calendar_id": str(cal_id),
        "user_id": str(share.user_id) if share.user_id else None,
        "group_id": str(share.group_id) if share.group_id else None,
        "permission": share.permission,
    }


@calendar_router.get("/{calendar_id}/permissions", dependencies=[Depends(require_permission("calendar:read"))])
async def get_permissions(
    calendar_id: str,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(get_current_user),
):
    """AC6: GET /api/v1/calendars/{id}/permissions → 200 + permission list."""
    tenant_id = uuid.UUID(current_user["tenant_id"])
    cal_id = _parse_uuid(calendar_id, "calendar_id")
    await _get_calendar_or_404(db, cal_id, tenant_id)
    result = await db.execute(select(CalendarShare).where(CalendarShare.calendar_id == cal_id))
    shares = result.scalars().all()
    return [
        {
            "id": str(s.id),
            "calendar_id": str(cal_id),
            "user_id": str(s.user_id) if s.user_id else None,
            "group_id": str(s.group_id) if s.group_id else None,
            "permission": s.permission,
        }
        for s in shares
    ]


# ─── Entries ───


@router.get("/calendar/entries", dependencies=[Depends(require_permission("calendar:read"))])
async def list_entries(
    start: str | None = None,
    end: str | None = None,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(get_current_user),
):
    """AC7: GET /api/v1/calendar/entries?start=...&end=... → 200 + entries in range."""
    tenant_id = uuid.UUID(current_user["tenant_id"])
    user_id = uuid.UUID(current_user["user_id"])
    role = current_user.get("role", "viewer")

    query = select(CalendarEntry).where(
        CalendarEntry.tenant_id == tenant_id,
        CalendarEntry.deleted_at.is_(None),
    )

    # Filter private entries: only owner + admin can see
    if role != "admin":
        query = query.where(
            (CalendarEntry.subtype != "private") | (CalendarEntry.created_by == user_id)
        )

    if start:
        try:
            start_dt = datetime.fromisoformat(start)
            query = query.where(CalendarEntry.start_at >= start_dt)
        except ValueError:
            pass
    if end:
        try:
            end_dt = datetime.fromisoformat(end)
            query = query.where(CalendarEntry.start_at <= end_dt)
        except ValueError:
            pass

    result = await db.execute(query.order_by(CalendarEntry.start_at))
    entries = result.scalars().all()

    # Generate recurrence occurrences
    all_occurrences: list[dict] = []
    for entry in entries:
        if entry.recurrence and entry.start_at:
            range_start = start_dt.date() if start else entry.start_at.date()
            range_end = end_dt.date() if end else datetime.now(UTC).date()
            # When end boundary is midnight, treat it as end-of-previous-day
            # (e.g. 2026-06-05T00:00:00 means up to end of June 4)
            if end and end_dt.time() == datetime.min.time():
                range_end = (end_dt - timedelta(days=1)).date()
            occs = generate_occurrences(entry.recurrence, entry.start_at, range_start, range_end)
            for occ in occs:
                occ_dict = _entry_to_dict(entry)
                occ_dict["start_at"] = occ.isoformat()
                occ_dict["occurrence_date"] = occ.isoformat()
                all_occurrences.append(occ_dict)
        else:
            all_occurrences.append(_entry_to_dict(entry))

    return all_occurrences


@router.post("/calendar/entries", status_code=status.HTTP_201_CREATED, dependencies=[Depends(require_permission("calendar:write"))])
async def create_entry(
    body: EntryCreate,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(get_current_user),
):
    """AC8/AC9: POST /api/v1/calendar/entries → 201, entry created (appointment or task)."""
    tenant_id = uuid.UUID(current_user["tenant_id"])
    user_id = uuid.UUID(current_user["user_id"])
    cal_id = _parse_uuid(body.calendar_id, "calendar_id")
    await _get_calendar_or_404(db, cal_id, tenant_id)

    if body.entry_type == "appointment":
        if not body.start_at:
            raise HTTPException(
                400, detail={"detail": "Appointment requires start_at", "code": "validation_error"}
            )
    elif body.entry_type == "task":
        if not body.due_date and not body.start_at:
            raise HTTPException(
                400,
                detail={"detail": "Task requires due_date or start_at", "code": "validation_error"},
            )

    assigned_to = _parse_uuid(body.assigned_to, "assigned_to") if body.assigned_to else None
    entry = CalendarEntry(
        tenant_id=tenant_id,
        calendar_id=cal_id,
        entry_type=body.entry_type,
        subtype=body.subtype,
        title=body.title,
        description=body.description,
        start_at=body.start_at,
        end_at=body.end_at,
        all_day=body.all_day,
        location=body.location,
        due_date=body.due_date,
        priority=body.priority,
        status=body.status,
        assigned_to=assigned_to,
        reminder=body.reminder,
        recurrence=body.recurrence,
        created_by=user_id,
    )
    db.add(entry)
    await db.flush()

    # Schedule reminder ARQ job if reminder is set
    if body.reminder:
        try:
            from app.core.jobs import enqueue_job

            reminder_time = body.reminder.get("value", 15)
            reminder_unit = body.reminder.get("unit", "minutes")
            if body.entry_type == "appointment" and body.start_at:
                fire_at = body.start_at
            elif body.entry_type == "task" and body.due_date:
                fire_at = body.due_date
            else:
                fire_at = None
            if fire_at:
                await enqueue_job(
                    "calendar_reminder",
                    entry_id=str(entry.id),
                    reminder_value=reminder_time,
                    reminder_unit=reminder_unit,
                )
        except Exception:
            pass

    return _entry_to_dict(entry)


@router.get("/calendar/entries/export", dependencies=[Depends(require_permission("calendar:read"))])
async def export_entries(
    format: str = "csv",
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(get_current_user),
):
    """AC19: GET /api/v1/calendar/entries/export?format=csv → 200 + CSV stream."""
    tenant_id = uuid.UUID(current_user["tenant_id"])
    user_id = uuid.UUID(current_user["user_id"])
    role = current_user.get("role", "viewer")

    query = select(CalendarEntry).where(
        CalendarEntry.tenant_id == tenant_id,
        CalendarEntry.deleted_at.is_(None),
    )
    if role != "admin":
        query = query.where(
            (CalendarEntry.subtype != "private") | (CalendarEntry.created_by == user_id)
        )
    result = await db.execute(query.order_by(CalendarEntry.start_at))
    entries = result.scalars().all()

    output = io.StringIO()
    writer = csv.writer(output)
    writer.writerow(
        [
            "id",
            "type",
            "subtype",
            "title",
            "start_at",
            "end_at",
            "due_date",
            "status",
            "priority",
            "location",
        ]
    )
    for e in entries:
        writer.writerow(
            [
                str(e.id),
                e.entry_type,
                e.subtype,
                e.title,
                e.start_at.isoformat() if e.start_at else "",
                e.end_at.isoformat() if e.end_at else "",
                e.due_date.isoformat() if e.due_date else "",
                e.status,
                e.priority,
                e.location or "",
            ]
        )
    output.seek(0)
    return StreamingResponse(
        iter([output.getvalue()]),
        media_type="text/csv",
        headers={"Content-Disposition": "attachment; filename=calendar_entries.csv"},
    )


@router.get("/calendar/entries/{entry_id}", dependencies=[Depends(require_permission("calendar:read"))])
async def get_entry(
    entry_id: str,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(get_current_user),
):
    """AC10: GET /api/v1/calendar/entries/{id} → 200 + entry detail with links+subtasks."""
    tenant_id = uuid.UUID(current_user["tenant_id"])
    user_id = uuid.UUID(current_user["user_id"])
    role = current_user.get("role", "viewer")
    eid = _parse_uuid(entry_id, "entry_id")
    entry = await _get_entry_or_404(db, eid, tenant_id)

    # Private entry access check
    if entry.subtype == "private" and entry.created_by != user_id and role != "admin":
        raise HTTPException(403, detail={"detail": "Private entry", "code": "forbidden"})

    # Fetch links
    link_result = await db.execute(
        select(CalendarEntryLink).where(CalendarEntryLink.entry_id == eid)
    )
    links = [
        {"id": str(lnk.id), "entity_type": lnk.entity_type, "entity_id": str(lnk.entity_id)}
        for lnk in link_result.scalars().all()
    ]

    # Fetch subtasks
    sub_result = await db.execute(select(Subtask).where(Subtask.entry_id == eid))
    subtasks = [
        {"id": str(s.id), "title": s.title, "completed": s.completed}
        for s in sub_result.scalars().all()
    ]

    return _entry_to_dict(entry, links, subtasks)


@router.patch("/calendar/entries/{entry_id}", dependencies=[Depends(require_permission("calendar:write"))])
async def update_entry(
    entry_id: str,
    body: EntryUpdate,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(get_current_user),
):
    """AC11/AC12: PATCH /api/v1/calendar/entries/{id} → 200, updated (drag&drop or status)."""
    tenant_id = uuid.UUID(current_user["tenant_id"])
    user_id = uuid.UUID(current_user["user_id"])
    role = current_user.get("role", "viewer")
    eid = _parse_uuid(entry_id, "entry_id")
    entry = await _get_entry_or_404(db, eid, tenant_id)

    # Permission check: owner, admin, or write share
    has_write = await _check_write_permission(db, entry.calendar_id, user_id, role)
    if not has_write:
        raise HTTPException(403, detail={"detail": "No write permission", "code": "forbidden"})

    # Apply updates
    if body.title is not None:
        entry.title = body.title
    if body.description is not None:
        entry.description = body.description
    if body.start_at is not None:
        entry.start_at = body.start_at
    if body.end_at is not None:
        entry.end_at = body.end_at
    if body.all_day is not None:
        entry.all_day = body.all_day
    if body.location is not None:
        entry.location = body.location
    if body.due_date is not None:
        entry.due_date = body.due_date
    if body.priority is not None:
        entry.priority = body.priority
    if body.status is not None:
        entry.status = body.status
    if body.assigned_to is not None:
        entry.assigned_to = _parse_uuid(body.assigned_to, "assigned_to")
    if body.subtype is not None:
        entry.subtype = body.subtype
    if body.reminder is not None:
        entry.reminder = body.reminder
    if body.recurrence is not None:
        entry.recurrence = body.recurrence
    if body.calendar_id is not None:
        new_cal_id = _parse_uuid(body.calendar_id, "calendar_id")
        await _get_calendar_or_404(db, new_cal_id, tenant_id)
        entry.calendar_id = new_cal_id

    await db.flush()
    await db.refresh(entry)
    return _entry_to_dict(entry)


@router.delete("/calendar/entries/{entry_id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(require_permission("calendar:delete"))])
async def delete_entry(
    entry_id: str,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(get_current_user),
):
    """AC13: DELETE /api/v1/calendar/entries/{id} → 204."""
    tenant_id = uuid.UUID(current_user["tenant_id"])
    user_id = uuid.UUID(current_user["user_id"])
    role = current_user.get("role", "viewer")
    eid = _parse_uuid(entry_id, "entry_id")
    entry = await _get_entry_or_404(db, eid, tenant_id)
    has_write = await _check_write_permission(db, entry.calendar_id, user_id, role)
    if not has_write:
        raise HTTPException(403, detail={"detail": "No write permission", "code": "forbidden"})
    entry.deleted_at = datetime.now(UTC)
    await db.flush()
    return Response(status_code=204)


@router.post("/calendar/entries/{entry_id}/link", dependencies=[Depends(require_permission("calendar:write"))])
async def link_entry(
    entry_id: str,
    body: LinkRequest,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(get_current_user),
):
    """AC14: POST /api/v1/calendar/entries/{id}/link → 200, linked to entity."""
    tenant_id = uuid.UUID(current_user["tenant_id"])
    eid = _parse_uuid(entry_id, "entry_id")
    await _get_entry_or_404(db, eid, tenant_id)
    link = CalendarEntryLink(
        tenant_id=tenant_id,
        entry_id=eid,
        entity_type=body.entity_type,
        entity_id=_parse_uuid(body.entity_id, "entity_id"),
    )
    db.add(link)
    await db.flush()
    return {
        "id": str(link.id),
        "entry_id": str(eid),
        "entity_type": link.entity_type,
        "entity_id": str(link.entity_id),
    }


@router.post("/calendar/entries/{entry_id}/subtasks", status_code=status.HTTP_201_CREATED, dependencies=[Depends(require_permission("calendar:write"))])
async def create_subtask(
    entry_id: str,
    body: SubtaskCreate,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(get_current_user),
):
    """AC15: POST /api/v1/calendar/entries/{id}/subtasks → 201, subtask created."""
    tenant_id = uuid.UUID(current_user["tenant_id"])
    eid = _parse_uuid(entry_id, "entry_id")
    await _get_entry_or_404(db, eid, tenant_id)
    sub = Subtask(
        tenant_id=tenant_id,
        entry_id=eid,
        title=body.title,
    )
    db.add(sub)
    await db.flush()
    return {
        "id": str(sub.id),
        "entry_id": str(eid),
        "title": sub.title,
        "completed": sub.completed,
    }


@router.patch("/calendar/entries/{entry_id}/subtasks/{sub_id}", dependencies=[Depends(require_permission("calendar:write"))])
async def update_subtask(
    entry_id: str,
    sub_id: str,
    body: SubtaskUpdate,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(get_current_user),
):
    """AC16: PATCH /api/v1/calendar/entries/{id}/subtasks/{sub_id} → 200, toggled."""
    tenant_id = uuid.UUID(current_user["tenant_id"])
    eid = _parse_uuid(entry_id, "entry_id")
    sid = _parse_uuid(sub_id, "sub_id")
    await _get_entry_or_404(db, eid, tenant_id)
    result = await db.execute(select(Subtask).where(Subtask.id == sid, Subtask.entry_id == eid))
    sub = result.scalar_one_or_none()
    if sub is None:
        raise HTTPException(404, detail={"detail": "Subtask not found", "code": "not_found"})
    if body.completed is not None:
        sub.completed = body.completed
    if body.title is not None:
        sub.title = body.title
    await db.flush()
    return {
        "id": str(sub.id),
        "entry_id": str(eid),
        "title": sub.title,
        "completed": sub.completed,
    }


# ─── Bulk + Kanban + Export ───


@router.post("/calendar/entries/bulk", dependencies=[Depends(require_permission("calendar:write"))])
async def bulk_action(
    body: BulkAction,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(get_current_user),
):
    """AC17: POST /api/v1/calendar/entries/bulk → 200, bulk status change/delete."""
    tenant_id = uuid.UUID(current_user["tenant_id"])
    entry_ids = [_parse_uuid(eid, "entry_id") for eid in body.entry_ids]

    if body.action == "delete":
        await db.execute(
            update(CalendarEntry)
            .where(
                CalendarEntry.id.in_(entry_ids),
                CalendarEntry.tenant_id == tenant_id,
                CalendarEntry.deleted_at.is_(None),
            )
            .values(deleted_at=datetime.now(UTC))
        )
        return {"action": "delete", "affected": len(entry_ids)}
    else:
        await db.execute(
            update(CalendarEntry)
            .where(
                CalendarEntry.id.in_(entry_ids),
                CalendarEntry.tenant_id == tenant_id,
                CalendarEntry.deleted_at.is_(None),
            )
            .values(status=body.action)
        )
        return {"action": body.action, "affected": len(entry_ids)}


@router.get("/calendar/kanban", dependencies=[Depends(require_permission("calendar:read"))])
async def kanban_board(
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(get_current_user),
):
    """AC18: GET /api/v1/calendar/kanban → 200 + tasks grouped by status columns."""
    tenant_id = uuid.UUID(current_user["tenant_id"])
    user_id = uuid.UUID(current_user["user_id"])
    role = current_user.get("role", "viewer")

    query = select(CalendarEntry).where(
        CalendarEntry.tenant_id == tenant_id,
        CalendarEntry.entry_type == "task",
        CalendarEntry.deleted_at.is_(None),
    )
    if role != "admin":
        query = query.where(
            (CalendarEntry.subtype != "private") | (CalendarEntry.created_by == user_id)
        )
    result = await db.execute(query)
    entries = result.scalars().all()

    columns = {"open": [], "in_progress": [], "done": [], "cancelled": []}
    for entry in entries:
        col = columns.get(entry.status, [])
        col.append(_entry_to_dict(entry))
    return columns


# ─── ICS Feed + Import ───


@router.get("/calendar/{calendar_id}/ics-feed", dependencies=[Depends(require_permission("calendar:read"))])
async def ics_feed(
    calendar_id: str,
    token: str | None = None,
    db: AsyncSession = Depends(get_db),
):
    """AC20/AC21: GET /api/v1/calendar/{calendar_id}/ics-feed?token=valid → 200 + text/calendar."""
    cal_id = _parse_uuid(calendar_id, "calendar_id")
    result = await db.execute(
        select(Calendar).where(Calendar.id == cal_id, Calendar.deleted_at.is_(None))
    )
    cal = result.scalar_one_or_none()
    if cal is None:
        raise HTTPException(404, detail={"detail": "Calendar not found", "code": "not_found"})

    # Generate token if not set
    if not cal.ics_token:
        cal.ics_token = secrets.token_hex(32)
        await db.flush()
        await db.commit()

    if token != cal.ics_token:
        raise HTTPException(401, detail={"detail": "Invalid token", "code": "invalid_token"})

    # Fetch entries
    entry_result = await db.execute(
        select(CalendarEntry).where(
            CalendarEntry.calendar_id == cal_id,
            CalendarEntry.deleted_at.is_(None),
            CalendarEntry.entry_type == "appointment",
        )
    )
    entries = entry_result.scalars().all()
    ics_content = export_entries_to_ics(entries)
    return Response(content=ics_content, media_type="text/calendar")


@router.post("/calendar/import", dependencies=[Depends(require_permission("calendar:write"))])
async def import_ics(
    file: UploadFile = File(...),
    calendar_id: str | None = None,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(get_current_user),
):
    """AC22: POST /api/v1/calendar/import (multipart .ics file) → 200 + import result."""
    tenant_id = uuid.UUID(current_user["tenant_id"])
    user_id = uuid.UUID(current_user["user_id"])

    # Determine target calendar
    if calendar_id:
        cal_id = _parse_uuid(calendar_id, "calendar_id")
    else:
        # Use first calendar or create default
        result = await db.execute(
            select(Calendar)
            .where(
                Calendar.tenant_id == tenant_id,
                Calendar.deleted_at.is_(None),
            )
            .limit(1)
        )
        cal = result.scalar_one_or_none()
        if cal is None:
            cal = Calendar(
                tenant_id=tenant_id,
                name="Imported",
                owner_id=user_id,
            )
            db.add(cal)
            await db.flush()
        cal_id = cal.id

    await _get_calendar_or_404(db, cal_id, tenant_id)

    content = await file.read()
    content_str = content.decode("utf-8")
    events = parse_ics(content_str)
    entry_data_list = ics_events_to_entry_data(events, cal_id, tenant_id, user_id)

    created = 0
    errors: list[str] = []
    for edata in entry_data_list:
        try:
            entry = CalendarEntry(**edata)
            db.add(entry)
            await db.flush()
            created += 1
        except Exception as exc:
            errors.append(str(exc))

    return {"entries_created": created, "errors": errors}


# ─── Resources ───


@resource_router.post("", status_code=status.HTTP_201_CREATED, dependencies=[Depends(require_permission("calendar:write"))])
async def create_resource(
    body: ResourceCreate,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(require_admin),
):
    """AC23: POST /api/v1/resources → 201 (admin only, 403 for non-admin)."""
    tenant_id = uuid.UUID(current_user["tenant_id"])
    resource = Resource(
        tenant_id=tenant_id,
        name=body.name,
        type=body.type,
    )
    db.add(resource)
    await db.flush()
    return {"id": str(resource.id), "name": resource.name, "type": resource.type}


@router.post("/calendar/entries/{entry_id}/book-resource", dependencies=[Depends(require_permission("calendar:write"))])
async def book_resource(
    entry_id: str,
    body: BookResourceRequest,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(get_current_user),
):
    """AC24/AC25: POST /api/v1/calendar/entries/{id}/book-resource → 200 or 409 (conflict)."""
    tenant_id = uuid.UUID(current_user["tenant_id"])
    eid = _parse_uuid(entry_id, "entry_id")
    resource_id = _parse_uuid(body.resource_id, "resource_id")
    entry = await _get_entry_or_404(db, eid, tenant_id)

    if not entry.start_at or not entry.end_at:
        raise HTTPException(
            400,
            detail={"detail": "Entry must have start_at and end_at", "code": "validation_error"},
        )

    # Check resource exists
    res_result = await db.execute(
        select(Resource).where(Resource.id == resource_id, Resource.tenant_id == tenant_id)
    )
    if res_result.scalar_one_or_none() is None:
        raise HTTPException(404, detail={"detail": "Resource not found", "code": "not_found"})

    # Check for conflicts (overlapping time ranges)
    conflict_result = await db.execute(
        select(ResourceBooking).where(
            ResourceBooking.resource_id == resource_id,
            ResourceBooking.tenant_id == tenant_id,
            ResourceBooking.start_at < entry.end_at,
            ResourceBooking.end_at > entry.start_at,
        )
    )
    if conflict_result.scalars().first() is not None:
        raise HTTPException(
            409, detail={"detail": "Resource already booked for this time", "code": "conflict"}
        )

    booking = ResourceBooking(
        tenant_id=tenant_id,
        resource_id=resource_id,
        entry_id=eid,
        start_at=entry.start_at,
        end_at=entry.end_at,
    )
    db.add(booking)
    await db.flush()
    return {
        "id": str(booking.id),
        "resource_id": str(resource_id),
        "entry_id": str(eid),
        "start_at": booking.start_at.isoformat(),
        "end_at": booking.end_at.isoformat(),
    }
