#!/usr/bin/env python3
"""
Night Scheduler - Leichtgewichtiger Scheduler für Nachtabschaltung

3-Projekte-Setup: Verwaltet Container aus breakpilot-core, breakpilot-lehrer, breakpilot-compliance.
Stoppt/Startet Container anhand des Namens-Patterns bp-*.
REST-API auf Port 8096 für Dashboard-Zugriff.
"""

import json
import os
import subprocess
import asyncio
from datetime import datetime, time, timedelta
from pathlib import Path
from typing import Optional
from contextlib import asynccontextmanager

from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field

# Konfiguration
CONFIG_PATH = Path("/config/night-mode.json")

# Compose-Dateien für alle 3 Projekte
COMPOSE_FILES = {
    "core": Path(os.getenv("COMPOSE_FILE_CORE", "/compose/breakpilot-core/docker-compose.yml")),
    "lehrer": Path(os.getenv("COMPOSE_FILE_LEHRER", "/compose/breakpilot-lehrer/docker-compose.yml")),
    "compliance": Path(os.getenv("COMPOSE_FILE_COMPLIANCE", "/compose/breakpilot-compliance/docker-compose.yml")),
}

# Container die NICHT gestoppt werden sollen
EXCLUDED_CONTAINERS = {"bp-core-night-scheduler", "bp-core-nginx"}


class NightModeConfig(BaseModel):
    """Konfiguration für den Nachtmodus"""
    enabled: bool = False
    shutdown_time: str = "22:00"
    startup_time: str = "06:00"
    last_action: Optional[str] = None
    last_action_time: Optional[str] = None
    excluded_services: list[str] = Field(default_factory=lambda: list(EXCLUDED_CONTAINERS))


class NightModeStatus(BaseModel):
    """Status-Response für die API"""
    config: NightModeConfig
    current_time: str
    next_action: Optional[str] = None
    next_action_time: Optional[str] = None
    time_until_next_action: Optional[str] = None
    services_status: dict[str, str] = Field(default_factory=dict)


class ExecuteRequest(BaseModel):
    """Request für sofortige Ausführung"""
    action: str  # "start" oder "stop"


def load_config() -> NightModeConfig:
    """Lädt die Konfiguration aus der JSON-Datei"""
    if CONFIG_PATH.exists():
        try:
            with open(CONFIG_PATH) as f:
                data = json.load(f)
                return NightModeConfig(**data)
        except (json.JSONDecodeError, Exception) as e:
            print(f"Fehler beim Laden der Konfiguration: {e}")
    return NightModeConfig()


def save_config(config: NightModeConfig) -> None:
    """Speichert die Konfiguration in die JSON-Datei"""
    CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
    with open(CONFIG_PATH, "w") as f:
        json.dump(config.model_dump(), f, indent=2)


def parse_time(time_str: str) -> time:
    """Parst einen Zeit-String im Format HH:MM"""
    parts = time_str.split(":")
    return time(int(parts[0]), int(parts[1]))


def get_all_bp_containers() -> list[dict]:
    """Ermittelt alle bp-* Container"""
    try:
        result = subprocess.run(
            ["docker", "ps", "-a", "--filter", "name=bp-", "--format",
             '{"name":"{{.Names}}","status":"{{.Status}}","state":"{{.State}}"}'],
            capture_output=True, text=True, timeout=30
        )
        if result.returncode == 0 and result.stdout.strip():
            containers = []
            for line in result.stdout.strip().split("\n"):
                if line:
                    try:
                        containers.append(json.loads(line))
                    except json.JSONDecodeError:
                        continue
            return containers
    except Exception as e:
        print(f"Fehler beim Ermitteln der Container: {e}")
    return []


def get_services_status() -> dict[str, str]:
    """Ermittelt den Status aller bp-* Container"""
    containers = get_all_bp_containers()
    return {c["name"]: c["state"] for c in containers}


def get_services_to_manage() -> list[str]:
    """Ermittelt alle Container, die verwaltet werden sollen"""
    containers = get_all_bp_containers()
    config = load_config()
    excluded = set(config.excluded_services)
    return [c["name"] for c in containers if c["name"] not in excluded]


def execute_docker_command(action: str) -> tuple[bool, str]:
    """
    Stoppt/Startet alle bp-* Container (außer excluded).
    Reihenfolge: Stop = Lehrer+Compliance zuerst, dann Core.
                 Start = Core zuerst, dann Lehrer+Compliance.
    """
    containers = get_services_to_manage()
    if not containers:
        return False, "Keine Container zum Verwalten gefunden"

    # Aufteilen nach Projekt
    core_containers = [c for c in containers if c.startswith("bp-core-")]
    lehrer_containers = [c for c in containers if c.startswith("bp-lehrer-")]
    compliance_containers = [c for c in containers if c.startswith("bp-compliance-")]

    errors = []
    total_managed = 0

    if action == "stop":
        # Erst Lehrer + Compliance stoppen, dann Core
        for batch_name, batch in [("lehrer", lehrer_containers), ("compliance", compliance_containers), ("core", core_containers)]:
            if batch:
                try:
                    cmd = ["docker", "stop"] + batch
                    print(f"Stoppe {batch_name}: {' '.join(batch)}")
                    result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
                    if result.returncode == 0:
                        total_managed += len(batch)
                    else:
                        errors.append(f"{batch_name}: {result.stderr}")
                except Exception as e:
                    errors.append(f"{batch_name}: {str(e)}")

    elif action == "start":
        # Erst Core starten, dann Lehrer + Compliance
        for batch_name, batch in [("core", core_containers), ("lehrer", lehrer_containers), ("compliance", compliance_containers)]:
            if batch:
                try:
                    cmd = ["docker", "start"] + batch
                    print(f"Starte {batch_name}: {' '.join(batch)}")
                    result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
                    if result.returncode == 0:
                        total_managed += len(batch)
                    else:
                        errors.append(f"{batch_name}: {result.stderr}")
                except Exception as e:
                    errors.append(f"{batch_name}: {str(e)}")
    else:
        return False, f"Unbekannte Aktion: {action}"

    if errors:
        return False, f"{total_managed} Container verwaltet, Fehler: {'; '.join(errors)}"

    return True, f"Aktion '{action}' erfolgreich für {total_managed} Container (Core + Lehrer + Compliance)"


def calculate_next_action(config: NightModeConfig) -> tuple[Optional[str], Optional[datetime], Optional[timedelta]]:
    """Berechnet die nächste Aktion basierend auf der aktuellen Zeit."""
    if not config.enabled:
        return None, None, None

    now = datetime.now()
    today = now.date()

    shutdown_time = parse_time(config.shutdown_time)
    startup_time = parse_time(config.startup_time)

    shutdown_dt = datetime.combine(today, shutdown_time)
    startup_dt = datetime.combine(today, startup_time)

    if startup_time < shutdown_time:
        if now.time() < startup_time:
            return "startup", startup_dt, startup_dt - now
        elif now.time() < shutdown_time:
            return "shutdown", shutdown_dt, shutdown_dt - now
        else:
            next_startup = startup_dt + timedelta(days=1)
            return "startup", next_startup, next_startup - now
    else:
        if now.time() < shutdown_time:
            return "shutdown", shutdown_dt, shutdown_dt - now
        elif now.time() < startup_time:
            return "startup", startup_dt, startup_dt - now
        else:
            next_shutdown = shutdown_dt + timedelta(days=1)
            return "shutdown", next_shutdown, next_shutdown - now


def format_timedelta(td: timedelta) -> str:
    """Formatiert ein timedelta als lesbaren String"""
    total_seconds = int(td.total_seconds())
    hours, remainder = divmod(total_seconds, 3600)
    minutes, _ = divmod(remainder, 60)
    if hours > 0:
        return f"{hours}h {minutes}min"
    return f"{minutes}min"


async def scheduler_loop():
    """Haupt-Scheduler-Schleife, prüft jede Minute"""
    print("Scheduler-Loop gestartet (3-Projekte-Modus: Core + Lehrer + Compliance)")

    while True:
        try:
            config = load_config()

            if config.enabled:
                now = datetime.now()
                current_time = now.time()

                shutdown_time = parse_time(config.shutdown_time)
                startup_time = parse_time(config.startup_time)

                if (current_time.hour == shutdown_time.hour and
                    current_time.minute == shutdown_time.minute):
                    if config.last_action != "shutdown" or (
                        config.last_action_time and
                        datetime.fromisoformat(config.last_action_time).date() < now.date()
                    ):
                        print(f"Shutdown-Zeit erreicht: {config.shutdown_time}")
                        success, msg = execute_docker_command("stop")
                        print(f"Shutdown: {msg}")
                        config.last_action = "shutdown"
                        config.last_action_time = now.isoformat()
                        save_config(config)

                elif (current_time.hour == startup_time.hour and
                      current_time.minute == startup_time.minute):
                    if config.last_action != "startup" or (
                        config.last_action_time and
                        datetime.fromisoformat(config.last_action_time).date() < now.date()
                    ):
                        print(f"Startup-Zeit erreicht: {config.startup_time}")
                        success, msg = execute_docker_command("start")
                        print(f"Startup: {msg}")
                        config.last_action = "startup"
                        config.last_action_time = now.isoformat()
                        save_config(config)

        except Exception as e:
            print(f"Fehler in Scheduler-Loop: {e}")

        await asyncio.sleep(60)


@asynccontextmanager
async def lifespan(app: FastAPI):
    """Lifecycle-Manager für FastAPI"""
    task = asyncio.create_task(scheduler_loop())
    yield
    task.cancel()
    try:
        await task
    except asyncio.CancelledError:
        pass


app = FastAPI(
    title="Night Scheduler API",
    description="Nachtabschaltung für 3-Projekte-Setup (Core + Lehrer + Compliance)",
    version="2.0.0",
    lifespan=lifespan
)

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


@app.get("/health")
async def health():
    """Health-Check-Endpoint"""
    containers = get_all_bp_containers()
    return {
        "status": "healthy",
        "service": "night-scheduler",
        "mode": "3-projects",
        "total_containers": len(containers)
    }


@app.get("/api/night-mode", response_model=NightModeStatus)
async def get_status():
    """Gibt den aktuellen Status und die Konfiguration zurück"""
    config = load_config()
    now = datetime.now()
    next_action, next_time, time_until = calculate_next_action(config)

    return NightModeStatus(
        config=config,
        current_time=now.strftime("%H:%M:%S"),
        next_action=next_action,
        next_action_time=next_time.strftime("%H:%M") if next_time else None,
        time_until_next_action=format_timedelta(time_until) if time_until else None,
        services_status=get_services_status()
    )


@app.post("/api/night-mode", response_model=NightModeConfig)
async def update_config(new_config: NightModeConfig):
    """Aktualisiert die Nachtmodus-Konfiguration"""
    try:
        parse_time(new_config.shutdown_time)
        parse_time(new_config.startup_time)
    except (ValueError, IndexError):
        raise HTTPException(status_code=400, detail="Ungültiges Zeitformat. Erwartet: HH:MM")

    if new_config.last_action is None:
        old_config = load_config()
        new_config.last_action = old_config.last_action
        new_config.last_action_time = old_config.last_action_time

    save_config(new_config)
    return new_config


@app.post("/api/night-mode/execute")
async def execute_action(request: ExecuteRequest):
    """Führt eine Aktion sofort aus (start/stop)"""
    if request.action not in ["start", "stop"]:
        raise HTTPException(status_code=400, detail="Aktion muss 'start' oder 'stop' sein")

    success, message = execute_docker_command(request.action)

    if success:
        config = load_config()
        config.last_action = "startup" if request.action == "start" else "shutdown"
        config.last_action_time = datetime.now().isoformat()
        save_config(config)
        return {"success": True, "message": message}
    else:
        raise HTTPException(status_code=500, detail=message)


@app.get("/api/night-mode/services")
async def get_services():
    """Gibt die Liste aller verwaltbaren Container zurück"""
    all_containers = get_all_bp_containers()
    config = load_config()
    excluded = set(config.excluded_services)

    # Gruppierung nach Projekt
    by_project = {"core": [], "lehrer": [], "compliance": [], "other": []}
    for c in all_containers:
        name = c["name"]
        if name.startswith("bp-core-"):
            by_project["core"].append(name)
        elif name.startswith("bp-lehrer-"):
            by_project["lehrer"].append(name)
        elif name.startswith("bp-compliance-"):
            by_project["compliance"].append(name)
        else:
            by_project["other"].append(name)

    return {
        "all_services": [c["name"] for c in all_containers],
        "by_project": by_project,
        "excluded_services": list(excluded),
        "status": get_services_status()
    }


@app.get("/api/night-mode/logs")
async def get_logs():
    """Gibt die letzten Aktionen zurück"""
    config = load_config()
    return {
        "last_action": config.last_action,
        "last_action_time": config.last_action_time,
        "enabled": config.enabled
    }


if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8096)
