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

Prüft jede Minute die Konfiguration und führt docker compose up/down aus.
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_FILE = Path("/app/docker-compose.yml")
COMPOSE_PROJECT = os.getenv("COMPOSE_PROJECT_NAME", "breakpilot-pwa")

# Services die NICHT gestoppt werden sollen
EXCLUDED_SERVICES = {"night-scheduler", "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  # "shutdown" oder "startup"
    last_action_time: Optional[str] = None
    excluded_services: list[str] = Field(default_factory=lambda: list(EXCLUDED_SERVICES))


class NightModeStatus(BaseModel):
    """Status-Response für die API"""
    config: NightModeConfig
    current_time: str
    next_action: Optional[str] = None  # "shutdown" oder "startup"
    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_services_to_manage() -> list[str]:
    """Ermittelt alle Services, die verwaltet werden sollen"""
    try:
        result = subprocess.run(
            ["docker", "compose", "-f", str(COMPOSE_FILE), "config", "--services"],
            capture_output=True,
            text=True,
            timeout=30
        )
        if result.returncode == 0:
            services = result.stdout.strip().split("\n")
            config = load_config()
            excluded = set(config.excluded_services)
            return [s for s in services if s and s not in excluded]
    except Exception as e:
        print(f"Fehler beim Ermitteln der Services: {e}")
    return []


def get_services_status() -> dict[str, str]:
    """Ermittelt den Status aller Services"""
    status = {}
    try:
        result = subprocess.run(
            ["docker", "compose", "-f", str(COMPOSE_FILE), "ps", "--format", "json"],
            capture_output=True,
            text=True,
            timeout=30
        )
        if result.returncode == 0 and result.stdout.strip():
            # Docker compose ps gibt JSON-Lines aus
            for line in result.stdout.strip().split("\n"):
                if line:
                    try:
                        container = json.loads(line)
                        service = container.get("Service", container.get("Name", "unknown"))
                        state = container.get("State", container.get("Status", "unknown"))
                        status[service] = state
                    except json.JSONDecodeError:
                        continue
    except Exception as e:
        print(f"Fehler beim Abrufen des Service-Status: {e}")
    return status


def execute_docker_command(action: str) -> tuple[bool, str]:
    """
    Führt docker compose Befehl aus.

    Args:
        action: "start" oder "stop"

    Returns:
        (success, message)
    """
    services = get_services_to_manage()
    if not services:
        return False, "Keine Services zum Verwalten gefunden"

    if action == "stop":
        cmd = ["docker", "compose", "-f", str(COMPOSE_FILE), "stop"] + services
    elif action == "start":
        cmd = ["docker", "compose", "-f", str(COMPOSE_FILE), "start"] + services
    else:
        return False, f"Unbekannte Aktion: {action}"

    try:
        print(f"Führe aus: {' '.join(cmd)}")
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)

        if result.returncode == 0:
            return True, f"Aktion '{action}' erfolgreich ausgeführt für {len(services)} Services"
        else:
            return False, f"Fehler: {result.stderr}"
    except subprocess.TimeoutExpired:
        return False, "Timeout beim Ausführen des Befehls"
    except Exception as e:
        return False, f"Ausnahme: {str(e)}"


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

    Returns:
        (action, next_time, time_until)
    """
    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)

    # Wenn startup vor shutdown ist, bedeutet das über Mitternacht
    # z.B. shutdown 22:00, startup 06:00
    if startup_time < shutdown_time:
        # Wir sind in der Nacht
        if now.time() < startup_time:
            # Vor Startup-Zeit -> nächste Aktion ist Startup heute
            return "startup", startup_dt, startup_dt - now
        elif now.time() < shutdown_time:
            # Zwischen Startup und Shutdown -> nächste Aktion ist Shutdown heute
            return "shutdown", shutdown_dt, shutdown_dt - now
        else:
            # Nach Shutdown -> nächste Aktion ist Startup morgen
            next_startup = startup_dt + timedelta(days=1)
            return "startup", next_startup, next_startup - now
    else:
        # Startup nach Shutdown am selben Tag (ungewöhnlich, aber unterstützt)
        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")

    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)

                # Prüfe ob Shutdown-Zeit erreicht
                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)

                # Prüfe ob Startup-Zeit erreicht
                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}")

        # Warte 60 Sekunden
        await asyncio.sleep(60)


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


# FastAPI App
app = FastAPI(
    title="Night Scheduler API",
    description="API für die Dashboard-gesteuerte Nachtabschaltung",
    version="1.0.0",
    lifespan=lifespan
)

# CORS für Admin-Dashboard
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


@app.get("/health")
async def health():
    """Health-Check-Endpoint"""
    return {"status": "healthy", "service": "night-scheduler"}


@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"""
    # Validiere Zeitformate
    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")

    # Behalte last_action bei, wenn nicht explizit gesetzt
    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:
        # Aktualisiere last_action
        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 Services zurück"""
    return {
        "all_services": get_services_to_manage(),
        "excluded_services": list(load_config().excluded_services),
        "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)
