# #region MaintenanceOrchestrators [C:4] [TYPE Module] [SEMANTICS maintenance, orchestrator, lifecycle]
# @BRIEF C4 orchestrators for maintenance event lifecycle: start, end, end-all.
#   Private helpers extracted to sub-modules. All orchestrators use belief runtime.
# @LAYER Service
# @RELATION DEPENDS_ON -> [MaintenanceModels]
# @RELATION DEPENDS_ON -> [SupersetClient]
# @RELATION DEPENDS_ON -> [MaintenanceDashboardScanner]
# @RELATION DEPENDS_ON -> [MaintenanceBannerRenderer]
# @RELATION DEPENDS_ON -> [MaintenanceChartManager]
# @INVARIANT C4 methods use belief_scope + REASON/REFLECT/EXPLORE markers.
# @INVARIANT Idempotency key: sorted lowercased (tables, start_time, end_time). message NOT in key.

from datetime import datetime
from typing import Any

from sqlalchemy.orm import Session

from ...core.logger import belief_scope, logger as app_logger
from ...core.superset_client import SupersetClient
from ...models.maintenance import (
    MaintenanceDashboardState,
    MaintenanceEvent,
    MaintenanceEventStatus,
    MaintenanceSettings,
)
from ._chart_manager import _process_dashboards_for_start, _process_states_for_end
from ._dashboard_scanner import find_affected_dashboards


# #region build_idempotency_key [C:2] [TYPE Function]
# @BRIEF Build idempotency key from (tables, start_time, end_time). Sorted lowercased tables, ISO timestamps.
# @PRE tables is a list of strings. start_time is a datetime or None. end_time is a datetime or None.
# @POST Returns a tuple of (frozenset(str), str|None, str|None) for DB comparison.
# @RATIONALE message is intentionally excluded from key per spec — different message with same tables/times = same event.
def build_idempotency_key(
    tables: list[str],
    start_time: datetime | None,
    end_time: datetime | None,
) -> tuple[frozenset[str], str | None, str | None]:
    sorted_tables = sorted(t.strip().lower() for t in tables if t.strip())
    start_key = start_time.isoformat() if start_time else None
    end_key = end_time.isoformat() if end_time else None
    return (frozenset(sorted_tables), start_key, end_key)
# #endregion build_idempotency_key


# #region start_maintenance [C:4] [TYPE Function] [SEMANTICS maintenance, start, orchestration, async]
# @BRIEF C4 orchestrator for starting a maintenance event. Runs inside a TaskManager task.
#   Idempotency check with SELECT ... FOR UPDATE. Creates event states, finds dashboards,
#   ensures banner charts, builds text, updates charts. Event transitions to ACTIVE or PARTIAL.
# @PRE event_id points to a valid MaintenanceEvent with status=PENDING.
# @POST Event and dashboard states transitioned to ACTIVE/PARTIAL/APPLY_FAILED.
#   Banner charts created and updated in Superset.
# @SIDE_EFFECT Modifies Superset dashboards (creates/updates charts, modifies layouts).
#   Writes multiple DB rows (dashboard states, banner updates).
# @BELIEF Uses belief_scope with REASON/REFLECT/EXPLORE markers.
# @RELATION CALLS -> [find_affected_dashboards]
# @RELATION CALLS -> [_process_dashboards_for_start]
def start_maintenance(
    event_id: str,
    db_session: Session,
    superset_client: SupersetClient,
) -> dict[str, Any]:
    with belief_scope("start_maintenance", f"event_id={event_id}"):
        app_logger.reason(
            "Starting maintenance event",
            extra={"event_id": event_id},
        )

        # Load event
        event = db_session.query(MaintenanceEvent).filter(
            MaintenanceEvent.id == event_id
        ).with_for_update().first()
        if not event:
            app_logger.explore(
                "Event not found",
                extra={"event_id": event_id},
                error="EventNotFound",
            )
            return {"maintenance_id": event_id, "status": "failed", "error": "Event not found"}

        if event.status != MaintenanceEventStatus.PENDING:
            app_logger.reflect(
                "Event already processed",
                extra={"event_id": event_id, "status": event.status.value},
            )
            return {"maintenance_id": event_id, "status": event.status.value}

        # Transition to APPLYING
        event.status = MaintenanceEventStatus.APPLYING
        db_session.flush()

        # Load settings
        settings = db_session.query(MaintenanceSettings).filter(
            MaintenanceSettings.id == "default"
        ).first()
        environment_id = settings.target_environment_id if settings else event.environment_id
        template = settings.banner_template if settings else ""
        timezone_str = settings.display_timezone if settings else "UTC"

        # Find affected dashboards
        try:
            dashboard_ids = find_affected_dashboards(
                event.tables,
                superset_client,
                settings,
            )
        except Exception as e:
            app_logger.explore(
                "Dashboard discovery failed",
                extra={"event_id": event_id},
                error=str(e),
            )
            event.status = MaintenanceEventStatus.FAILED
            db_session.flush()
            return {
                "maintenance_id": event_id,
                "status": "failed",
                "error": str(e),
                "affected_dashboards": 0,
                "dashboards": [],
                "failed_dashboards": [],
                "unmatched_tables": [],
            }

        if not dashboard_ids:
            app_logger.reflect(
                "No matching dashboards found",
                extra={"event_id": event_id},
            )
            event.status = MaintenanceEventStatus.ACTIVE
            db_session.flush()
            return {
                "maintenance_id": event_id,
                "status": "no_match",
                "affected_dashboards": 0,
                "dashboards": [],
                "failed_dashboards": [],
                "unmatched_tables": [
                    t for t in event.tables if t not in dashboard_ids
                ],
            }

        # Process each dashboard
        successful_dashboards, failed_dashboards, _ = _process_dashboards_for_start(
            dashboard_ids,
            event_id,
            environment_id,
            template,
            timezone_str,
            superset_client,
            db_session,
        )

        # Determine final event status
        if not failed_dashboards:
            event.status = MaintenanceEventStatus.ACTIVE
        elif successful_dashboards:
            event.status = MaintenanceEventStatus.PARTIAL
        else:
            event.status = MaintenanceEventStatus.FAILED

        db_session.flush()

        result = {
            "maintenance_id": event_id,
            "status": event.status.value,
            "affected_dashboards": len(successful_dashboards),
            "dashboards": successful_dashboards,
            "failed_dashboards": failed_dashboards,
            "unmatched_tables": [],
        }

        app_logger.reflect(
            "Maintenance start completed",
            extra={
                "event_id": event_id,
                "status": event.status.value,
                "successful": len(successful_dashboards),
                "failed": len(failed_dashboards),
            },
        )
        return result
# #endregion start_maintenance


# #region end_maintenance [C:4] [TYPE Function] [SEMANTICS maintenance, end, orchestration, async, removal]
# @BRIEF C4 orchestrator for ending a maintenance event. Runs inside a TaskManager task.
#   Event → ENDING. Delegates per-state processing to _process_states_for_end.
#   Event → COMPLETED when done.
# @PRE event_id points to a valid active MaintenanceEvent.
# @POST Event transitions to COMPLETED. Dashboard states cleaned up. Charts removed when no other events.
# @SIDE_EFFECT Modifies Superset dashboards (removes charts, updates layouts). Writes DB.
# @BELIEF Uses belief_scope with REASON/REFLECT/EXPLORE markers.
# @RELATION CALLS -> [_process_states_for_end]
def end_maintenance(
    event_id: str,
    db_session: Session,
    superset_client: SupersetClient,
) -> dict[str, Any]:
    with belief_scope("end_maintenance", f"event_id={event_id}"):
        app_logger.reason(
            "Ending maintenance event",
            extra={"event_id": event_id},
        )

        # Load event with lock
        event = db_session.query(MaintenanceEvent).filter(
            MaintenanceEvent.id == event_id
        ).with_for_update().first()
        if not event:
            app_logger.explore(
                "Event not found",
                extra={"event_id": event_id},
                error="EventNotFound",
            )
            return {"maintenance_id": event_id, "status": "failed", "error": "Event not found"}

        # Idempotent: already completed
        if event.status == MaintenanceEventStatus.COMPLETED:
            app_logger.reflect(
                "Event already completed, idempotent",
                extra={"event_id": event_id},
            )
            return {"maintenance_id": event_id, "status": "already_completed", "removed_from": 0, "failed_removals": []}

        # Transition to ENDING
        event.status = MaintenanceEventStatus.ENDING
        db_session.flush()

        # Find all dashboard states for this event
        states = db_session.query(MaintenanceDashboardState).filter(
            MaintenanceDashboardState.event_id == event_id
        ).all()

        # Delegate per-state processing
        removed_count, failed_removals = _process_states_for_end(
            states, event_id, superset_client, db_session,
        )

        # Transition event to COMPLETED (even if some removals failed)
        event.status = MaintenanceEventStatus.COMPLETED
        db_session.flush()

        result = {
            "maintenance_id": event_id,
            "status": "completed" if not failed_removals else "partial",
            "removed_from": removed_count,
            "failed_removals": failed_removals,
        }

        app_logger.reflect(
            "Maintenance end completed",
            extra={
                "event_id": event_id,
                "removed": removed_count,
                "failed": len(failed_removals),
            },
        )
        return result
# #endregion end_maintenance


# #region end_all_maintenance [C:4] [TYPE Function] [SEMANTICS maintenance, end-all, orchestration, bulk]
# @BRIEF End ALL active maintenance events. Iterates active events, calls end_maintenance for each.
# @PRE None.
# @POST All events ended. All banners removed or rebuilt (last event removed).
# @SIDE_EFFECT Modifies multiple Superset dashboards. Writes multiple DB rows.
# @BELIEF Uses belief_scope with REASON/REFLECT/EXPLORE markers.
# @RELATION CALLS -> [end_maintenance]
def end_all_maintenance(
    db_session: Session,
    superset_client: SupersetClient,
) -> dict[str, Any]:
    with belief_scope("end_all_maintenance"):
        app_logger.reason("Ending all active maintenance events")

        active_events = db_session.query(MaintenanceEvent).filter(
            MaintenanceEvent.status.in_([
                MaintenanceEventStatus.ACTIVE,
                MaintenanceEventStatus.PARTIAL,
                MaintenanceEventStatus.APPLYING,
            ])
        ).all()

        if not active_events:
            app_logger.reflect("No active events to end")
            return {
                "status": "completed",
                "closed_events": 0,
                "removed_from": 0,
                "failed_removals": [],
            }

        total_removed = 0
        all_failed: list[dict[str, Any]] = []
        closed_count = 0

        for event in active_events:
            app_logger.reason(
                "Ending event",
                extra={"event_id": event.id},
            )
            try:
                result = end_maintenance(
                    event.id, db_session, superset_client
                )
                total_removed += result.get("removed_from", 0)
                all_failed.extend(result.get("failed_removals", []))
                closed_count += 1
            except Exception as e:
                app_logger.explore(
                    "Failed to end event",
                    extra={"event_id": event.id},
                    error=str(e),
                )
                all_failed.append(
                    {"id": 0, "title": event.id, "error": str(e)}
                )

        app_logger.reflect(
            "All events ended",
            extra={
                "closed": closed_count,
                "removed": total_removed,
                "failed": len(all_failed),
            },
        )

        return {
            "status": "completed" if not all_failed else "partial",
            "closed_events": closed_count,
            "removed_from": total_removed,
            "failed_removals": all_failed,
        }
# #endregion end_all_maintenance

# #endregion MaintenanceOrchestrators
