# #region MaintenanceRoutesModule [C:3] [TYPE Module] [SEMANTICS fastapi, routes, maintenance, api]
# @BRIEF Route handler stubs for all 7 Maintenance Banner API endpoints with RBAC guards per FR-015.
# @LAYER API
# @RELATION DEPENDS_ON -> [MaintenanceSchemasModule]
# @RELATION DEPENDS_ON -> [MaintenanceRouter]
# @RELATION DEPENDS_ON -> [AppDependencies]
# @RELATION DEPENDS_ON -> [EXT:frontend:MaintenanceServiceModule]
# @INVARIANT All mutation endpoints return 202 {task_id} per spec.
# @INVARIANT RBAC enforced per FR-015 matrix via has_permission() dependency.

from datetime import UTC, datetime
from typing import Any, cast

from fastapi import Depends, HTTPException, Request, status
from sqlalchemy.orm import Session

from ....core.logger import belief_scope, logger as app_logger
from ....core.task_manager import TaskManager

app_logger = cast(Any, app_logger)
from ....dependencies import (
    check_api_key_environment_scope,
    get_db,
    get_task_manager,
    has_permission,
    require_api_key_or_jwt,
)
from ....models.api_key import APIKey
from ....models.maintenance import (
    MaintenanceDashboardBanner,
    MaintenanceDashboardBannerStatus,
    MaintenanceDashboardState,
    MaintenanceDashboardStateStatus,
    MaintenanceEvent,
    MaintenanceEventStatus,
    MaintenanceSettings,
)
from ._router import router
from ._schemas import (
    MaintenanceAlreadyActiveResponse,
    MaintenanceDashboardBannerState,
    MaintenanceEndResponse,
    MaintenanceEventItem,
    MaintenanceEventListResponse,
    MaintenanceSettingsResponse,
    MaintenanceSettingsUpdate,
    MaintenanceStartRequest,
    MaintenanceStartResponse,
)

# ── GET /api/maintenance/dashboard-banners ───────────────────
# FR-015: viewer, operator, admin all have READ access

# #region list_dashboard_banners [C:2] [TYPE Function]
# @BRIEF Get per-dashboard banner state for the Dashboard Hub indicator.
# @RELATION DEPENDS_ON -> [EXT:code:has_permission_maintenance_READ]
@router.get("/dashboard-banners", response_model=list[MaintenanceDashboardBannerState])
async def list_dashboard_banners(
    db: Session = Depends(get_db),
    _=Depends(has_permission("maintenance", "READ")),
):
    with belief_scope("list_dashboard_banners"):
        # Query active banners with their linked active/pending states
        active_banners = (
            db.query(MaintenanceDashboardBanner)
            .filter(MaintenanceDashboardBanner.status == MaintenanceDashboardBannerStatus.ACTIVE)
            .all()
        )

        result: list[MaintenanceDashboardBannerState] = []
        for banner in active_banners:
            linked_states = (
                db.query(MaintenanceDashboardState)
                .filter(
                    MaintenanceDashboardState.banner_id == banner.id,
                    MaintenanceDashboardState.status.in_([
                        MaintenanceDashboardStateStatus.ACTIVE,
                        MaintenanceDashboardStateStatus.PENDING_APPLY,
                    ]),
                )
                .all()
            )

            event_ids: list[str] = []
            start_times: list[str] = []
            end_times: list[str] = []

            for state in linked_states:
                event = (
                    db.query(MaintenanceEvent)
                    .filter(MaintenanceEvent.id == state.event_id)
                    .first()
                )
                if event:
                    event_ids.append(event.id)
                    if event.start_time:
                        start_times.append(event.start_time.isoformat())
                    if event.end_time:
                        end_times.append(event.end_time.isoformat())

            result.append(
                MaintenanceDashboardBannerState(
                    dashboard_id=banner.dashboard_id,
                    active=True,
                    events=event_ids,
                    start_time=min(start_times) if start_times else None,
                    end_time=max(end_times) if end_times else None,
                )
            )

        return result
# #endregion list_dashboard_banners


# ── GET /api/maintenance/events ──────────────────────────────
# FR-015: operator, admin (NOT viewer)

# #region list_events [C:2] [TYPE Function]
# @BRIEF Get active and completed maintenance event lists with affected dashboard counts.
# @RELATION DEPENDS_ON -> [EXT:code:has_permission_maintenance_READ]
@router.get("/events", response_model=MaintenanceEventListResponse)
async def list_events(
    db: Session = Depends(get_db),
    _=Depends(has_permission("maintenance", "READ")),
    task_manager: TaskManager = Depends(get_task_manager),
):
    with belief_scope("list_events"):
        # ── Auto-expiry: end events past their end_time ──
        now = datetime.now(UTC)
        expired = (
            db.query(MaintenanceEvent)
            .filter(
                MaintenanceEvent.status.in_([
                    MaintenanceEventStatus.ACTIVE,
                    MaintenanceEventStatus.PARTIAL,
                    MaintenanceEventStatus.ENDING,
                ]),
                MaintenanceEvent.end_time.isnot(None),
                MaintenanceEvent.end_time < now,
            )
            .all()
        )
        for ev in expired:
            app_logger.reason(
                "Auto-ending expired maintenance event",
                extra={"event_id": ev.id, "end_time": ev.end_time.isoformat()},
            )
            # Schedule proper cleanup via task manager (removes banner from dashboard)
            task = await task_manager.create_task(
                plugin_id="maintenance_banner_apply",
                params={
                    "operation": "end",
                    "event_id": ev.id,
                    "environment_id": ev.environment_id,
                    "user": "system",
                },
            )
            app_logger.reflect(
                "Scheduled cleanup for expired event",
                extra={"event_id": ev.id, "task_id": task.id},
            )

        # Active events: ACTIVE, PARTIAL (per spec T040)
        active_statuses = [
            MaintenanceEventStatus.ACTIVE,
            MaintenanceEventStatus.PARTIAL,
        ]
        active_events = (
            db.query(MaintenanceEvent)
            .filter(MaintenanceEvent.status.in_(active_statuses))
            .order_by(MaintenanceEvent.created_at.desc())
            .all()
        )

        # Completed events: COMPLETED, FAILED
        completed_events = (
            db.query(MaintenanceEvent)
            .filter(
                MaintenanceEvent.status.in_([
                    MaintenanceEventStatus.COMPLETED,
                    MaintenanceEventStatus.FAILED,
                ])
            )
            .order_by(MaintenanceEvent.created_at.desc())
            .limit(50)
            .all()
        )

        def _to_item(event: MaintenanceEvent) -> MaintenanceEventItem:
            affected_count = (
                db.query(MaintenanceDashboardState)
                .filter(
                    MaintenanceDashboardState.event_id == event.id,
                    MaintenanceDashboardState.status.in_([
                        MaintenanceDashboardStateStatus.ACTIVE,
                        MaintenanceDashboardStateStatus.PENDING_APPLY,
                    ]),
                )
                .count()
            )
            return MaintenanceEventItem(
                id=event.id,
                tables=list(event.tables) if event.tables else [],
                start_time=event.start_time.isoformat() if event.start_time else "",
                end_time=event.end_time.isoformat() if event.end_time else None,
                message=event.message or None,
                status=event.status.value,
                affected_count=affected_count,
                created_at=event.created_at.isoformat() if event.created_at else "",
            )

        return MaintenanceEventListResponse(
            active=[_to_item(e) for e in active_events],
            completed=[_to_item(e) for e in completed_events],
        )
# #endregion list_events


# ── POST /api/maintenance/start ──────────────────────────────
# FR-001: Always returns 202 {task_id} after validation
# FR-015: operator, admin (NOT viewer)

# #region start_maintenance [C:3] [TYPE Function]
# @BRIEF Start a maintenance event. Validates input, creates event, dispatches TaskManager task.
#   Always returns 202 with task_id and maintenance_id.
#   Idempotency: same (tables, start_time, end_time) returns already_active (409-like but 200).
#   Tables sorted & lowered for idempotency key. Message NOT in key.
# @RELATION DEPENDS_ON -> [EXT:code:has_permission("maintenance", "WRITE")]
# @RELATION DEPENDS_ON -> [TaskManager]
@router.post(
    "/start",
    status_code=status.HTTP_202_ACCEPTED,
    responses={
        202: {"model": MaintenanceStartResponse},
        409: {"model": MaintenanceAlreadyActiveResponse},
    },
)
async def start_maintenance(
    request: MaintenanceStartRequest,
    db: Session = Depends(get_db),
    _auth=Depends(require_api_key_or_jwt("maintenance:start", "maintenance", "WRITE")),
    task_manager: TaskManager = Depends(get_task_manager),
):
    with belief_scope("start_maintenance"):
        # ── Validation ──

        # Validate end_time > start_time
        if request.end_time and request.start_time and request.end_time <= request.start_time:
            raise HTTPException(
                status_code=400,
                detail="end_time must be after start_time",
            )

        # Validate times are within reasonable range (future ±1h grace period)
        now = datetime.now(UTC)
        grace_threshold = 3600  # 1 hour in seconds
        if request.start_time:
            diff = (request.start_time - now).total_seconds() if request.start_time.tzinfo else \
                   (request.start_time.replace(tzinfo=UTC) - now).total_seconds()
            if diff < -grace_threshold:
                raise HTTPException(
                    status_code=400,
                    detail="start_time is too far in the past (max 1h grace period)",
                )

        # Validate tables list length
        if len(request.tables) > 100:
            raise HTTPException(
                status_code=400,
                detail="Maximum 100 tables allowed",
            )

        # Sanitize message (HTML escape already done in rendering, but check for raw HTML)
        message = request.message
        if message:
            import html as _html
            sanitized = _html.escape(message)
            if sanitized != message:
                # Message contained HTML — accept it, it will be escaped at render time
                pass

        # ── Idempotency check ──

        sorted_tables = sorted(t.strip().lower() for t in request.tables if t.strip())
        idem_tables = frozenset(sorted_tables)
        idem_start = request.start_time.isoformat() if request.start_time else None
        idem_end = request.end_time.isoformat() if request.end_time else None

        # Find existing active event with same idempotency key
        active_statuses = [
            MaintenanceEventStatus.PENDING,
            MaintenanceEventStatus.APPLYING,
            MaintenanceEventStatus.ACTIVE,
            MaintenanceEventStatus.PARTIAL,
        ]
        existing = (
            db.query(MaintenanceEvent)
            .filter(
                MaintenanceEvent.status.in_(active_statuses),
            )
            .all()
        )
        for ev in existing:
            ev_tables = frozenset(
                sorted(t.lower() for t in (ev.tables or []) if t)
            )
            ev_start = ev.start_time.isoformat() if ev.start_time else None
            ev_end = ev.end_time.isoformat() if ev.end_time else None
            if ev_tables == idem_tables and ev_start == idem_start and ev_end == idem_end:
                # Same key → return already_active
                app_logger.reflect(
                    "Idempotency hit — event already active",
                    extra={"existing_id": ev.id},
                )
                return MaintenanceAlreadyActiveResponse(
                    maintenance_id=ev.id,
                    status="already_active",
                )

        # ── Environment scoping ──
        env_id = request.environment_id

        # ── Create event ──
        event = MaintenanceEvent(
            tables=sorted_tables,
            start_time=request.start_time,
            end_time=request.end_time,
            message=message,
            status=MaintenanceEventStatus.PENDING,
            environment_id=env_id,
        )
        db.add(event)
        db.flush()

        # ── Dispatch TaskManager task ──
        task = await task_manager.create_task(
            plugin_id="maintenance_banner_apply",
            params={
                "operation": "start",
                "event_id": event.id,
                "environment_id": env_id,
                "user": _auth.split(":", 1)[1] if ":" in _auth else _auth,
            },
        )
        event.task_id = task.id
        db.commit()

        app_logger.reflect(
            "Maintenance start dispatched",
            extra={"event_id": event.id, "task_id": task.id},
        )

        # ── Broadcast maintenance event ──
        task_manager.broadcast_maintenance_event({
            "type": "maintenance.event_created",
            "maintenance_id": event.id,
            "tables": sorted_tables,
            "start_time": event.start_time.isoformat() if event.start_time else None,
            "end_time": event.end_time.isoformat() if event.end_time else None,
            "message": message,
            "environment_id": env_id,
        })

        return MaintenanceStartResponse(
            task_id=task.id,
            maintenance_id=event.id,
            status="pending",
        )
# #endregion start_maintenance


# ── POST /api/maintenance/{maintenance_id}/end ───────────────
# FR-006: Remove banners for a specific maintenance event
# FR-015: operator, admin (NOT viewer)

# #region end_maintenance [C:3] [TYPE Function]
# @BRIEF End a specific maintenance event by ID. Removes banners from affected dashboards.
#   Always returns 202 with task_id. Idempotent: already_completed returns success.
# @RELATION DEPENDS_ON -> [EXT:code:has_permission("maintenance", "WRITE")]
@router.post(
    "/{maintenance_id}/end",
    status_code=status.HTTP_202_ACCEPTED,
    response_model=MaintenanceEndResponse,
)
async def end_maintenance(
    maintenance_id: str,
    request: Request,
    db: Session = Depends(get_db),
    _auth=Depends(require_api_key_or_jwt("maintenance:end", "maintenance", "WRITE")),
    task_manager: TaskManager = Depends(get_task_manager),
):
    with belief_scope("end_maintenance", f"maintenance_id={maintenance_id}"):
        # Check event exists
        event = db.query(MaintenanceEvent).filter(
            MaintenanceEvent.id == maintenance_id
        ).first()

        if not event:
            raise HTTPException(
                status_code=404,
                detail=f"Maintenance event {maintenance_id} not found",
            )

        # Validate API key environment scope against event's environment
        if _auth.startswith("api_key:"):
            await check_api_key_environment_scope(request, db, event.environment_id or "")

        # If already completed, return idempotent success
        if event.status == MaintenanceEventStatus.COMPLETED:
            app_logger.reflect(
                "Event already completed, returning idempotent",
                extra={"event_id": maintenance_id},
            )
            return MaintenanceEndResponse(
                task_id=event.task_id or "",
                status="already_completed",
            )

        # ── Dispatch TaskManager task ──
        task = await task_manager.create_task(
            plugin_id="maintenance_banner_apply",
            params={
                "operation": "end",
                "event_id": maintenance_id,
                "environment_id": event.environment_id,
                "user": _auth.split(":", 1)[1] if ":" in _auth else _auth,
            },
        )

        app_logger.reflect(
            "Maintenance end dispatched",
            extra={"event_id": maintenance_id, "task_id": task.id},
        )

        # ── Broadcast maintenance event ──
        task_manager.broadcast_maintenance_event({
            "type": "maintenance.event_ended",
            "maintenance_id": maintenance_id,
        })

        return MaintenanceEndResponse(
            task_id=task.id,
            status="pending",
        )
# #endregion end_maintenance


# ── POST /api/maintenance/end-all ────────────────────────────
# FR-007: Bulk removal of all active banners
# FR-015: operator, admin (NOT viewer)

# #region end_all_maintenance [C:3] [TYPE Function]
# @BRIEF End all active maintenance events. Removes all banners from all dashboards.
#   Always returns 202 with task_id. Supports environment scoping for API keys.
# @RELATION DEPENDS_ON -> [EXT:code:has_permission("maintenance", "WRITE")]
@router.post(
    "/end-all",
    status_code=status.HTTP_202_ACCEPTED,
    response_model=MaintenanceEndResponse,
)
async def end_all_maintenance(
    request: Request,
    db: Session = Depends(get_db),
    _auth=Depends(require_api_key_or_jwt("maintenance:end_all", "maintenance", "WRITE")),
    task_manager: TaskManager = Depends(get_task_manager),
):
    with belief_scope("end_all_maintenance"):
        # ── Resolve environment scope ──
        effective_env_id: str | None = None
        try:
            body = await request.json()
            effective_env_id = body.get("environment_id")
        except Exception:
            pass

        # ── Enforce API key environment scoping ──
        if _auth.startswith("api_key:"):
            # Auto-scope to the key's environment if not specified in body
            if not effective_env_id:
                raw_key = request.headers.get("X-API-Key")
                if raw_key:
                    from ....core.auth.api_key import hash_api_key
                    key_hash = hash_api_key(raw_key)
                    key_row = db.query(APIKey).filter(APIKey.key_hash == key_hash).first()
                    if key_row and key_row.environment_id:
                        effective_env_id = key_row.environment_id

            await check_api_key_environment_scope(request, db, effective_env_id or "")

        # ── Dispatch TaskManager task ──
        task = await task_manager.create_task(
            plugin_id="maintenance_banner_apply",
            params={
                "operation": "end_all",
                "environment_id": effective_env_id or "",
                "user": _auth.split(":", 1)[1] if ":" in _auth else _auth,
            },
        )

        app_logger.reflect(
            "End-all maintenance dispatched",
            extra={"task_id": task.id, "environment_id": effective_env_id},
        )

        # ── Broadcast maintenance event ──
        task_manager.broadcast_maintenance_event({
            "type": "maintenance.events_ended_all",
            "environment_id": effective_env_id or "",
        })

        return MaintenanceEndResponse(
            task_id=task.id,
            status="pending",
        )
# #endregion end_all_maintenance


# ── GET /api/maintenance/settings ────────────────────────────
# FR-009: Get maintenance settings configuration
# FR-015: operator, admin (NOT viewer)

# #region get_maintenance_settings [C:2] [TYPE Function]
# @BRIEF Get current maintenance settings configuration.
# @RELATION DEPENDS_ON -> [EXT:code:has_permission_maintenance_READ]
@router.get("/settings", response_model=MaintenanceSettingsResponse)
async def get_maintenance_settings(
    db: Session = Depends(get_db),
    _=Depends(has_permission("maintenance", "READ")),
):
    with belief_scope("get_maintenance_settings"):
        settings = db.query(MaintenanceSettings).filter(
            MaintenanceSettings.id == "default"
        ).first()

        if not settings:
            raise HTTPException(
                status_code=404,
                detail="Maintenance settings not configured",
            )

        return MaintenanceSettingsResponse(
            target_environment_id=settings.target_environment_id,
            display_timezone=settings.display_timezone,
            banner_template=settings.banner_template,
            dashboard_scope=settings.dashboard_scope.value,
            excluded_dashboard_ids=list(settings.excluded_dashboard_ids or []),
            forced_dashboard_ids=list(settings.forced_dashboard_ids or []),
            updated_at=settings.updated_at.isoformat() if settings.updated_at else None,
        )
# #endregion get_maintenance_settings


# ── PUT /api/maintenance/settings ────────────────────────────
# FR-009: Update maintenance settings configuration
# FR-015: admin ONLY (NOT operator, NOT viewer)

# #region update_maintenance_settings [C:2] [TYPE Function]
# @BRIEF Update maintenance settings. All fields optional for partial update.
#   admin role required per FR-015.
# @RELATION DEPENDS_ON -> [EXT:code:has_permission("admin:settings", "WRITE")]
@router.put("/settings", response_model=MaintenanceSettingsResponse)
async def update_maintenance_settings(
    settings_data: MaintenanceSettingsUpdate,
    db: Session = Depends(get_db),
    _=Depends(has_permission("admin:settings", "WRITE")),
):
    with belief_scope("update_maintenance_settings"):
        settings = db.query(MaintenanceSettings).filter(
            MaintenanceSettings.id == "default"
        ).first()

        if not settings:
            # Auto-create with defaults on first save (upsert behavior).
            # Column-level defaults from the model apply for omitted fields.
            settings = MaintenanceSettings(
                id="default",
                target_environment_id=settings_data.target_environment_id or "",
            )
            db.add(settings)
            db.flush()

        # Apply partial updates
        update_data = settings_data.model_dump(exclude_unset=True)

        if "target_environment_id" in update_data:
            settings.target_environment_id = update_data["target_environment_id"]
        if "display_timezone" in update_data:
            settings.display_timezone = update_data["display_timezone"]
        if "banner_template" in update_data:
            settings.banner_template = update_data["banner_template"]
        if "dashboard_scope" in update_data:
            from ....models.maintenance import DashboardScope
            settings.dashboard_scope = DashboardScope(update_data["dashboard_scope"])
        if "excluded_dashboard_ids" in update_data:
            settings.excluded_dashboard_ids = update_data["excluded_dashboard_ids"]
        if "forced_dashboard_ids" in update_data:
            settings.forced_dashboard_ids = update_data["forced_dashboard_ids"]

        db.commit()
        db.refresh(settings)

        return MaintenanceSettingsResponse(
            target_environment_id=settings.target_environment_id,
            display_timezone=settings.display_timezone,
            banner_template=settings.banner_template,
            dashboard_scope=settings.dashboard_scope.value,
            excluded_dashboard_ids=list(settings.excluded_dashboard_ids or []),
            forced_dashboard_ids=list(settings.forced_dashboard_ids or []),
            updated_at=settings.updated_at.isoformat() if settings.updated_at else None,
        )
# #endregion update_maintenance_settings

# #endregion MaintenanceRoutesModule
