"""System routes: backup, restore, export, import, auto-backup.

Extracted from system.py to keep files under 800 lines.
"""

import asyncio
import io
import json
import subprocess
import sys
import threading
from datetime import datetime, timezone
from pathlib import Path

from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile
from fastapi.responses import StreamingResponse

from wled_controller import __version__
from wled_controller.api.auth import AuthRequired
from wled_controller.api.dependencies import get_auto_backup_engine
from wled_controller.api.schemas.system import (
    AutoBackupSettings,
    AutoBackupStatusResponse,
    BackupFileInfo,
    BackupListResponse,
    RestoreResponse,
)
from wled_controller.core.backup.auto_backup import AutoBackupEngine
from wled_controller.config import get_config
from wled_controller.utils import atomic_write_json, get_logger

logger = get_logger(__name__)

router = APIRouter()

# ---------------------------------------------------------------------------
# Configuration backup / restore
# ---------------------------------------------------------------------------

# Mapping: logical store name -> StorageConfig attribute name
STORE_MAP = {
    "devices": "devices_file",
    "capture_templates": "templates_file",
    "postprocessing_templates": "postprocessing_templates_file",
    "picture_sources": "picture_sources_file",
    "output_targets": "output_targets_file",
    "pattern_templates": "pattern_templates_file",
    "color_strip_sources": "color_strip_sources_file",
    "audio_sources": "audio_sources_file",
    "audio_templates": "audio_templates_file",
    "value_sources": "value_sources_file",
    "sync_clocks": "sync_clocks_file",
    "color_strip_processing_templates": "color_strip_processing_templates_file",
    "automations": "automations_file",
    "scene_presets": "scene_presets_file",
}

_SERVER_DIR = Path(__file__).resolve().parents[4]


def _schedule_restart() -> None:
    """Spawn a restart script after a short delay so the HTTP response completes."""

    def _restart():
        import time
        time.sleep(1)
        if sys.platform == "win32":
            subprocess.Popen(
                ["powershell", "-ExecutionPolicy", "Bypass", "-File",
                 str(_SERVER_DIR / "restart.ps1")],
                creationflags=subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP,
            )
        else:
            subprocess.Popen(
                ["bash", str(_SERVER_DIR / "restart.sh")],
                start_new_session=True,
            )

    threading.Thread(target=_restart, daemon=True).start()


@router.get("/api/v1/system/export/{store_key}", tags=["System"])
def export_store(store_key: str, _: AuthRequired):
    """Download a single entity store as a JSON file."""
    if store_key not in STORE_MAP:
        raise HTTPException(
            status_code=404,
            detail=f"Unknown store '{store_key}'. Valid keys: {sorted(STORE_MAP.keys())}",
        )
    config = get_config()
    file_path = Path(getattr(config.storage, STORE_MAP[store_key]))
    if file_path.exists():
        with open(file_path, "r", encoding="utf-8") as f:
            data = json.load(f)
    else:
        data = {}

    export = {
        "meta": {
            "format": "ledgrab-partial-export",
            "format_version": 1,
            "store_key": store_key,
            "app_version": __version__,
            "created_at": datetime.now(timezone.utc).isoformat() + "Z",
        },
        "store": data,
    }
    content = json.dumps(export, indent=2, ensure_ascii=False)
    timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H%M%S")
    filename = f"ledgrab-{store_key}-{timestamp}.json"
    return StreamingResponse(
        io.BytesIO(content.encode("utf-8")),
        media_type="application/json",
        headers={"Content-Disposition": f'attachment; filename="{filename}"'},
    )


@router.post("/api/v1/system/import/{store_key}", tags=["System"])
async def import_store(
    store_key: str,
    _: AuthRequired,
    file: UploadFile = File(...),
    merge: bool = Query(False, description="Merge into existing data instead of replacing"),
):
    """Upload a partial export file to replace or merge one entity store. Triggers server restart."""
    if store_key not in STORE_MAP:
        raise HTTPException(
            status_code=404,
            detail=f"Unknown store '{store_key}'. Valid keys: {sorted(STORE_MAP.keys())}",
        )

    try:
        raw = await file.read()
        if len(raw) > 10 * 1024 * 1024:
            raise HTTPException(status_code=400, detail="File too large (max 10 MB)")
        payload = json.loads(raw)
    except json.JSONDecodeError as e:
        raise HTTPException(status_code=400, detail=f"Invalid JSON: {e}")

    # Support both full-backup format and partial-export format
    if "stores" in payload and isinstance(payload.get("meta"), dict):
        # Full backup: extract the specific store
        if payload["meta"].get("format") not in ("ledgrab-backup",):
            raise HTTPException(status_code=400, detail="Not a valid LED Grab backup or partial export file")
        stores = payload.get("stores", {})
        if store_key not in stores:
            raise HTTPException(status_code=400, detail=f"Backup does not contain store '{store_key}'")
        incoming = stores[store_key]
    elif isinstance(payload.get("meta"), dict) and payload["meta"].get("format") == "ledgrab-partial-export":
        # Partial export format
        if payload["meta"].get("store_key") != store_key:
            raise HTTPException(
                status_code=400,
                detail=f"File is for store '{payload['meta']['store_key']}', not '{store_key}'",
            )
        incoming = payload.get("store", {})
    else:
        raise HTTPException(status_code=400, detail="Not a valid LED Grab backup or partial export file")

    if not isinstance(incoming, dict):
        raise HTTPException(status_code=400, detail="Store data must be a JSON object")

    config = get_config()
    file_path = Path(getattr(config.storage, STORE_MAP[store_key]))

    def _write():
        if merge and file_path.exists():
            with open(file_path, "r", encoding="utf-8") as f:
                existing = json.load(f)
            if isinstance(existing, dict):
                existing.update(incoming)
                atomic_write_json(file_path, existing)
                return len(existing)
        atomic_write_json(file_path, incoming)
        return len(incoming)

    count = await asyncio.to_thread(_write)
    logger.info(f"Imported store '{store_key}' ({count} entries, merge={merge}). Scheduling restart...")
    _schedule_restart()
    return {
        "status": "imported",
        "store_key": store_key,
        "entries": count,
        "merge": merge,
        "restart_scheduled": True,
        "message": f"Imported {count} entries for '{store_key}'. Server restarting...",
    }


@router.get("/api/v1/system/backup", tags=["System"])
def backup_config(_: AuthRequired):
    """Download all configuration as a single JSON backup file."""
    config = get_config()
    stores = {}
    for store_key, config_attr in STORE_MAP.items():
        file_path = Path(getattr(config.storage, config_attr))
        if file_path.exists():
            with open(file_path, "r", encoding="utf-8") as f:
                stores[store_key] = json.load(f)
        else:
            stores[store_key] = {}

    backup = {
        "meta": {
            "format": "ledgrab-backup",
            "format_version": 1,
            "app_version": __version__,
            "created_at": datetime.now(timezone.utc).isoformat() + "Z",
            "store_count": len(stores),
        },
        "stores": stores,
    }

    content = json.dumps(backup, indent=2, ensure_ascii=False)
    timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H%M%S")
    filename = f"ledgrab-backup-{timestamp}.json"

    return StreamingResponse(
        io.BytesIO(content.encode("utf-8")),
        media_type="application/json",
        headers={"Content-Disposition": f'attachment; filename="{filename}"'},
    )


@router.post("/api/v1/system/restart", tags=["System"])
def restart_server(_: AuthRequired):
    """Schedule a server restart and return immediately."""
    _schedule_restart()
    return {"status": "restarting"}


@router.post("/api/v1/system/restore", response_model=RestoreResponse, tags=["System"])
async def restore_config(
    _: AuthRequired,
    file: UploadFile = File(...),
):
    """Upload a backup file to restore all configuration. Triggers server restart."""
    # Read and parse
    try:
        raw = await file.read()
        if len(raw) > 10 * 1024 * 1024:  # 10 MB limit
            raise HTTPException(status_code=400, detail="Backup file too large (max 10 MB)")
        backup = json.loads(raw)
    except json.JSONDecodeError as e:
        raise HTTPException(status_code=400, detail=f"Invalid JSON file: {e}")

    # Validate envelope
    meta = backup.get("meta")
    if not isinstance(meta, dict) or meta.get("format") != "ledgrab-backup":
        raise HTTPException(status_code=400, detail="Not a valid LED Grab backup file")

    fmt_version = meta.get("format_version", 0)
    if fmt_version > 1:
        raise HTTPException(
            status_code=400,
            detail=f"Backup format version {fmt_version} is not supported by this server version",
        )

    stores = backup.get("stores")
    if not isinstance(stores, dict):
        raise HTTPException(status_code=400, detail="Backup file missing 'stores' section")

    known_keys = set(STORE_MAP.keys())
    present_keys = known_keys & set(stores.keys())
    if not present_keys:
        raise HTTPException(status_code=400, detail="Backup contains no recognized store data")

    for key in present_keys:
        if not isinstance(stores[key], dict):
            raise HTTPException(status_code=400, detail=f"Store '{key}' in backup is not a valid JSON object")

    # Write store files atomically (in thread to avoid blocking event loop)
    config = get_config()

    def _write_stores():
        count = 0
        for store_key, config_attr in STORE_MAP.items():
            if store_key in stores:
                file_path = Path(getattr(config.storage, config_attr))
                atomic_write_json(file_path, stores[store_key])
                count += 1
                logger.info(f"Restored store: {store_key} -> {file_path}")
        return count

    written = await asyncio.to_thread(_write_stores)

    logger.info(f"Restore complete: {written}/{len(STORE_MAP)} stores written. Scheduling restart...")
    _schedule_restart()

    missing = known_keys - present_keys
    return RestoreResponse(
        status="restored",
        stores_written=written,
        stores_total=len(STORE_MAP),
        missing_stores=sorted(missing) if missing else [],
        restart_scheduled=True,
        message=f"Restored {written} stores. Server restarting...",
    )


# ---------------------------------------------------------------------------
# Auto-backup settings & saved backups
# ---------------------------------------------------------------------------


@router.get(
    "/api/v1/system/auto-backup/settings",
    response_model=AutoBackupStatusResponse,
    tags=["System"],
)
async def get_auto_backup_settings(
    _: AuthRequired,
    engine: AutoBackupEngine = Depends(get_auto_backup_engine),
):
    """Get auto-backup settings and status."""
    return engine.get_settings()


@router.put(
    "/api/v1/system/auto-backup/settings",
    response_model=AutoBackupStatusResponse,
    tags=["System"],
)
async def update_auto_backup_settings(
    _: AuthRequired,
    body: AutoBackupSettings,
    engine: AutoBackupEngine = Depends(get_auto_backup_engine),
):
    """Update auto-backup settings (enable/disable, interval, max backups)."""
    return await engine.update_settings(
        enabled=body.enabled,
        interval_hours=body.interval_hours,
        max_backups=body.max_backups,
    )


@router.post("/api/v1/system/auto-backup/trigger", tags=["System"])
async def trigger_backup(
    _: AuthRequired,
    engine: AutoBackupEngine = Depends(get_auto_backup_engine),
):
    """Manually trigger a backup now."""
    backup = await engine.trigger_backup()
    return {"status": "ok", "backup": backup}


@router.get(
    "/api/v1/system/backups",
    response_model=BackupListResponse,
    tags=["System"],
)
async def list_backups(
    _: AuthRequired,
    engine: AutoBackupEngine = Depends(get_auto_backup_engine),
):
    """List all saved backup files."""
    backups = engine.list_backups()
    return BackupListResponse(
        backups=[BackupFileInfo(**b) for b in backups],
        count=len(backups),
    )


@router.get("/api/v1/system/backups/{filename}", tags=["System"])
def download_saved_backup(
    filename: str,
    _: AuthRequired,
    engine: AutoBackupEngine = Depends(get_auto_backup_engine),
):
    """Download a specific saved backup file."""
    try:
        path = engine.get_backup_path(filename)
    except (ValueError, FileNotFoundError) as e:
        raise HTTPException(status_code=404, detail=str(e))

    content = path.read_bytes()
    return StreamingResponse(
        io.BytesIO(content),
        media_type="application/json",
        headers={"Content-Disposition": f'attachment; filename="{filename}"'},
    )


@router.delete("/api/v1/system/backups/{filename}", tags=["System"])
async def delete_saved_backup(
    filename: str,
    _: AuthRequired,
    engine: AutoBackupEngine = Depends(get_auto_backup_engine),
):
    """Delete a specific saved backup file."""
    try:
        engine.delete_backup(filename)
    except (ValueError, FileNotFoundError) as e:
        raise HTTPException(status_code=404, detail=str(e))
    return {"status": "deleted", "filename": filename}
