"""FastAPI application entry point."""

import sys
from contextlib import asynccontextmanager
from pathlib import Path

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware
from fastapi.responses import FileResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from starlette.requests import Request

from ledgrab import __version__, GITEA_BASE_URL, GITEA_REPO, paths as _paths
from ledgrab.api import router
from ledgrab.api.dependencies import init_dependencies
from ledgrab.config import get_config
from ledgrab.core.processing.processor_manager import (
    ProcessorDependencies,
    ProcessorManager,
)
from ledgrab.storage import DeviceStore
from ledgrab.storage.template_store import TemplateStore
from ledgrab.storage.postprocessing_template_store import PostprocessingTemplateStore
from ledgrab.storage.picture_source_store import PictureSourceStore
from ledgrab.storage.output_target_store import OutputTargetStore
from ledgrab.storage.color_strip_store import ColorStripStore
from ledgrab.storage.audio_source_store import AudioSourceStore
from ledgrab.storage.audio_template_store import AudioTemplateStore
import ledgrab.core.audio  # noqa: F401 — trigger engine auto-registration
from ledgrab.storage.value_source_store import ValueSourceStore
from ledgrab.storage.automation_store import AutomationStore
from ledgrab.storage.scene_preset_store import ScenePresetStore
from ledgrab.storage.sync_clock_store import SyncClockStore
from ledgrab.storage.color_strip_processing_template_store import (
    ColorStripProcessingTemplateStore,
)
from ledgrab.storage.gradient_store import GradientStore
from ledgrab.storage.weather_source_store import WeatherSourceStore
from ledgrab.storage.asset_store import AssetStore
from ledgrab.core.processing.sync_clock_manager import SyncClockManager
from ledgrab.core.weather.weather_manager import WeatherManager
from ledgrab.storage.home_assistant_store import HomeAssistantStore
from ledgrab.core.home_assistant.ha_manager import HomeAssistantManager
from ledgrab.core.automations.automation_engine import AutomationEngine
from ledgrab.storage.game_integration_store import GameIntegrationStore
from ledgrab.core.game_integration.event_bus import GameEventBus
import ledgrab.core.game_integration.adapters  # noqa: F401 — register built-in adapters
from ledgrab.core.game_integration.community_loader import register_community_adapters
from ledgrab.core.mqtt.mqtt_manager import MQTTManager
from ledgrab.storage.mqtt_source_store import MQTTSourceStore
from ledgrab.storage.audio_processing_template_store import AudioProcessingTemplateStore
from ledgrab.storage.pattern_template_store import PatternTemplateStore
import ledgrab.core.audio.filters  # noqa: F401 — trigger audio filter auto-registration
from ledgrab.core.backup.auto_backup import AutoBackupEngine
from ledgrab.core.processing.os_notification_listener import OsNotificationListener
from ledgrab.core.devices.discovery_watcher import DiscoveryWatcher
from ledgrab.core.update.update_service import UpdateService
from ledgrab.core.update.gitea_provider import GiteaReleaseProvider
from ledgrab.storage.database import Database
from ledgrab.utils import setup_logging, get_logger, install_broadcast_handler

# Initialize logging
setup_logging()
install_broadcast_handler()
logger = get_logger(__name__)

# Get configuration
config = get_config()


def _migrate_legacy_data_location() -> None:
    """Rescue data from pre-rename cwd-relative paths.

    Older versions (and dev runs from inside ``server/``) wrote the database
    and assets to ``<cwd>/data/``.  If the configured database location is
    empty but a legacy path has data, copy it over so the user's data
    follows them to the platform-standard location.
    """
    import shutil

    db_path = Path(config.storage.database_file)
    if db_path.exists():
        return  # configured location already populated — nothing to do

    for legacy_db in _paths.legacy_db_candidates():
        if not legacy_db.is_file():
            continue
        try:
            # Skip if legacy is the same file we were going to open.
            if db_path.parent.exists() and legacy_db.resolve() == db_path.resolve():
                continue
        except OSError:
            continue
        if legacy_db.stat().st_size < 4096:
            # 4 KiB is roughly a freshly-initialised SQLite file with no
            # user data — skip so an empty dev DB doesn't shadow a real one.
            continue

        logger.warning(
            "Migrating database from legacy location %s -> %s. "
            "The original file is kept in place; you may delete it once you "
            "confirm the new location works.",
            legacy_db,
            db_path,
        )
        db_path.parent.mkdir(parents=True, exist_ok=True)
        shutil.copy2(legacy_db, db_path)
        # Copy WAL/SHM side-files too so uncheckpointed writes come along.
        for suffix in ("-wal", "-shm"):
            side = legacy_db.with_name(legacy_db.name + suffix)
            if side.exists():
                shutil.copy2(side, db_path.with_name(db_path.name + suffix))

        # Also migrate assets dir if the configured one is missing.
        assets_dir = Path(config.assets.assets_dir)
        legacy_assets = legacy_db.parent / "assets"
        if not assets_dir.exists() and legacy_assets.is_dir():
            logger.warning(
                "Migrating assets from legacy location %s -> %s",
                legacy_assets,
                assets_dir,
            )
            shutil.copytree(legacy_assets, assets_dir)
        return


_migrate_legacy_data_location()

# Initialize SQLite database
db = Database(config.storage.database_file)

# Seed demo data after DB is ready (first-run only)
if config.demo:
    from ledgrab.core.demo_seed import seed_demo_data

    seed_demo_data(db)

# Initialize storage and processing
device_store = DeviceStore(db)
template_store = TemplateStore(db)
pp_template_store = PostprocessingTemplateStore(db)
picture_source_store = PictureSourceStore(db)
output_target_store = OutputTargetStore(db)
color_strip_store = ColorStripStore(db)
audio_source_store = AudioSourceStore(db)
audio_template_store = AudioTemplateStore(db)
value_source_store = ValueSourceStore(db)
automation_store = AutomationStore(db)
scene_preset_store = ScenePresetStore(db)
sync_clock_store = SyncClockStore(db)
cspt_store = ColorStripProcessingTemplateStore(db)
gradient_store = GradientStore(db)
gradient_store.migrate_palette_references(color_strip_store)
weather_source_store = WeatherSourceStore(db)
asset_store = AssetStore(db, config.assets.assets_dir)

# Import prebuilt notification sounds on first run
_prebuilt_sounds_dir = Path(__file__).parent / "data" / "prebuilt_sounds"
asset_store.import_prebuilt_sounds(_prebuilt_sounds_dir)

sync_clock_manager = SyncClockManager(sync_clock_store)
weather_manager = WeatherManager(weather_source_store)
ha_store = HomeAssistantStore(db)
ha_manager = HomeAssistantManager(ha_store)
mqtt_source_store = MQTTSourceStore(db)
mqtt_manager = MQTTManager(mqtt_source_store)
audio_processing_template_store = AudioProcessingTemplateStore(db)
game_integration_store = GameIntegrationStore(db)
pattern_template_store = PatternTemplateStore(db)
game_event_bus = GameEventBus()
register_community_adapters()

processor_manager = ProcessorManager(
    ProcessorDependencies(
        picture_source_store=picture_source_store,
        capture_template_store=template_store,
        pp_template_store=pp_template_store,
        device_store=device_store,
        color_strip_store=color_strip_store,
        audio_source_store=audio_source_store,
        value_source_store=value_source_store,
        audio_template_store=audio_template_store,
        sync_clock_manager=sync_clock_manager,
        cspt_store=cspt_store,
        gradient_store=gradient_store,
        weather_manager=weather_manager,
        asset_store=asset_store,
        ha_manager=ha_manager,
        mqtt_manager=mqtt_manager,
        game_event_bus=game_event_bus,
        audio_processing_template_store=audio_processing_template_store,
    )
)


def _save_all_stores() -> None:
    """Shutdown hook — SQLite stores use write-through caching, so this is a no-op.

    Every create/update/delete already goes to the database immediately.
    Kept for backward compatibility with server_ref.py which calls this.
    """
    logger.info("Shutdown: all stores already persisted (write-through cache)")


@asynccontextmanager
async def lifespan(app: FastAPI):
    """Application lifespan manager.

    Handles startup and shutdown events.
    """
    # Startup
    logger.info(f"Starting LED Grab v{__version__}")
    logger.info(f"Python version: {sys.version}")
    logger.info(f"Server listening on {config.server.host}:{config.server.port}")
    print("\n  =============================================")
    print(f"   LED Grab v{__version__}")
    print(f"   Open http://localhost:{config.server.port} in your browser")
    print("  =============================================\n")

    # Log authentication mode
    if not config.auth.api_keys:
        logger.info(
            "Authentication: loopback-only (no API keys configured — "
            "LAN requests will be rejected with 401)"
        )
        # Loud warning when bound to a non-loopback address
        if config.server.host not in ("127.0.0.1", "::1", "localhost"):
            logger.critical(
                "SECURITY: server bound to %s but no API key set; "
                "LAN requests will be rejected. Add auth.api_keys entries "
                "in config.yaml to enable LAN access.",
                config.server.host,
            )
    else:
        logger.info(f"Authentication enabled ({len(config.auth.api_keys)} API key(s) configured)")
        client_labels = ", ".join(config.auth.api_keys.keys())
        logger.info(f"Authorized clients: {client_labels}")

    # One-shot migration: legacy global ``mqtt:`` config block → first MQTTSource.
    # No-op once the store has any entries.
    try:
        from ledgrab.core.mqtt.legacy_migration import migrate_legacy_mqtt_config

        migrate_legacy_mqtt_config(mqtt_source_store)
    except Exception as e:
        logger.error("Legacy MQTT migration failed: %s", e)

    # Create automation engine (needs processor_manager + MQTT manager + stores for scene activation)
    automation_engine = AutomationEngine(
        automation_store,
        processor_manager,
        scene_preset_store=scene_preset_store,
        target_store=output_target_store,
        device_store=device_store,
        ha_manager=ha_manager,
        mqtt_manager=mqtt_manager,
    )

    # Create auto-backup engine — derive paths from database location so that
    # demo mode auto-backups go to data/demo/ instead of data/.
    _data_dir = Path(config.storage.database_file).parent
    auto_backup_engine = AutoBackupEngine(
        backup_dir=_data_dir / "backups",
        db=db,
    )

    # Create update service (checks for new releases)
    _release_provider = GiteaReleaseProvider(
        base_url=GITEA_BASE_URL,
        repo=GITEA_REPO,
    )
    update_service = UpdateService(
        provider=_release_provider,
        db=db,
        fire_event=processor_manager.fire_event,
        update_dir=_data_dir / "updates",
    )

    # Initialize API dependencies
    init_dependencies(
        device_store,
        template_store,
        processor_manager,
        database=db,
        pp_template_store=pp_template_store,
        picture_source_store=picture_source_store,
        output_target_store=output_target_store,
        color_strip_store=color_strip_store,
        audio_source_store=audio_source_store,
        audio_template_store=audio_template_store,
        value_source_store=value_source_store,
        automation_store=automation_store,
        scene_preset_store=scene_preset_store,
        automation_engine=automation_engine,
        auto_backup_engine=auto_backup_engine,
        sync_clock_store=sync_clock_store,
        sync_clock_manager=sync_clock_manager,
        cspt_store=cspt_store,
        gradient_store=gradient_store,
        weather_source_store=weather_source_store,
        weather_manager=weather_manager,
        update_service=update_service,
        asset_store=asset_store,
        ha_store=ha_store,
        ha_manager=ha_manager,
        game_integration_store=game_integration_store,
        game_event_bus=game_event_bus,
        mqtt_store=mqtt_source_store,
        mqtt_manager=mqtt_manager,
        audio_processing_template_store=audio_processing_template_store,
        pattern_template_store=pattern_template_store,
    )

    # Register devices in processor manager for health monitoring
    devices = device_store.get_all_devices()
    for device in devices:
        try:
            processor_manager.add_device(
                device_id=device.id,
                device_url=device.url,
                led_count=device.led_count,
                device_type=device.device_type,
                baud_rate=device.baud_rate,
                software_brightness=device.software_brightness,
                auto_shutdown=device.auto_shutdown,
            )
            logger.info(f"Registered device: {device.name} ({device.id})")
        except Exception as e:
            logger.error(f"Failed to register device {device.id}: {e}")

    logger.info(f"Registered {len(devices)} devices for health monitoring")

    # Register output targets in processor manager
    targets = output_target_store.get_all_targets()
    registered_targets = 0
    for target in targets:
        try:
            target.register_with_manager(processor_manager)
            registered_targets += 1
            logger.info(f"Registered target: {target.name} ({target.id})")
        except Exception as e:
            logger.error(f"Failed to register target {target.id}: {e}")

    logger.info(f"Registered {registered_targets} output target(s)")

    # Start background health monitoring for all devices
    await processor_manager.start_health_monitoring()

    # Start automation engine (evaluates conditions and activates scenes)
    await automation_engine.start()

    # Start auto-backup engine (periodic configuration backups)
    await auto_backup_engine.start()

    # Start update checker (periodic release polling)
    await update_service.start()

    # Start OS notification listener (Windows toast → notification CSS streams)
    os_notif_listener = OsNotificationListener(
        color_strip_store=color_strip_store,
        color_strip_stream_manager=processor_manager.color_strip_stream_manager,
    )
    os_notif_listener.start()

    # Start background discovery watcher (mDNS + serial polling).
    # Gated by user pref; default is on. The watcher emits
    # device_discovered/device_lost events through the same fire_event
    # bus that the health monitor uses for device_health_changed.
    from ledgrab.api.routes.preferences import load_notification_preferences

    discovery_watcher: DiscoveryWatcher | None = None
    try:
        notif_prefs = load_notification_preferences(db)
        if notif_prefs.background_discovery_enabled:
            discovery_watcher = DiscoveryWatcher(
                device_store=device_store,
                fire_event=processor_manager.fire_event,
            )
            await discovery_watcher.start()
    except Exception as e:
        logger.error(f"Failed to start discovery watcher: {e}")

    yield

    # Shutdown
    logger.info("Shutting down LED Grab")

    # Persist all stores to disk before stopping anything.
    # This ensures in-memory data survives force-kills and restarts
    # where no CRUD happened during the session.
    _save_all_stores()

    # Stop automation engine first so it can no longer activate scenes that
    # would talk to processors mid-shutdown.
    try:
        await automation_engine.stop()
        logger.info("Stopped automation engine")
    except Exception as e:
        logger.error(f"Error stopping automation engine: {e}")

    # Stop discovery watcher and OS notification listener so they stop
    # firing events into a shutting-down processor manager.
    if discovery_watcher is not None:
        try:
            await discovery_watcher.stop()
        except Exception as e:
            logger.error(f"Error stopping discovery watcher: {e}")

    try:
        os_notif_listener.stop()
    except Exception as e:
        logger.error(f"Error stopping OS notification listener: {e}")

    # Stop all processing BEFORE tearing down ha_manager / mqtt_manager.
    # HA-light targets need a live HA runtime to apply their stop_action
    # (turn_off / restore), and MQTT-output targets need a live broker
    # runtime to send restore frames. Shutting those down first silently
    # turns "stop_targets" into a no-op for those targets.
    #
    # The shutdown_action setting controls whether per-device restore
    # frames are sent: "stop_targets" (default) runs the normal stop
    # sequence; "nothing" cancels capture tasks so the LEDs freeze on
    # their last frame.
    try:
        from ledgrab.api.routes.system_settings import load_shutdown_action

        action = load_shutdown_action(db)
    except Exception as e:
        logger.error(f"Error reading shutdown action setting, defaulting to stop_targets: {e}")
        action = "stop_targets"

    logger.info("Shutdown action: %s", action)
    try:
        await processor_manager.stop_all(restore_devices=action != "nothing")
        logger.info("Stopped all processors")
    except Exception as e:
        logger.error(f"Error stopping processors: {e}")

    # Now safe to tear down the connections that processors depended on.
    try:
        await ha_manager.shutdown()
    except Exception as e:
        logger.error(f"Error stopping Home Assistant manager: {e}")

    try:
        await mqtt_manager.shutdown()
    except Exception as e:
        logger.error(f"Error stopping MQTT manager: {e}")

    # Independent services — order doesn't matter relative to processors.
    try:
        weather_manager.shutdown()
    except Exception as e:
        logger.error(f"Error stopping weather manager: {e}")

    try:
        await update_service.stop()
    except Exception as e:
        logger.error(f"Error stopping update checker: {e}")

    try:
        await auto_backup_engine.stop()
    except Exception as e:
        logger.error(f"Error stopping auto-backup engine: {e}")


# Create FastAPI application
app = FastAPI(
    title="LED Grab",
    description="Control WLED devices based on screen content for ambient lighting",
    version=__version__,
    lifespan=lifespan,
    docs_url="/docs",
    redoc_url="/redoc",
    openapi_url="/openapi.json",
)

# Configure CORS
app.add_middleware(
    CORSMiddleware,
    allow_origins=config.server.cors_origins,
    allow_credentials=False,  # API uses Bearer tokens, not cookies
    allow_methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
    allow_headers=["Content-Type", "Authorization", "X-Api-Key"],
)
app.add_middleware(GZipMiddleware, minimum_size=500)

# Include API routes
app.include_router(router)

# PWA: serve manifest and service worker from root scope
_static_root = Path(__file__).parent / "static"


@app.get("/manifest.json", include_in_schema=False)
async def pwa_manifest():
    """Serve PWA manifest from root scope."""
    return FileResponse(_static_root / "manifest.json", media_type="application/manifest+json")


@app.get("/sw.js", include_in_schema=False)
async def pwa_service_worker():
    """Serve service worker from root scope (controls all pages)."""
    return FileResponse(
        _static_root / "sw.js",
        media_type="application/javascript",
        headers={"Cache-Control": "no-cache"},
    )


# Middleware: no-cache for static JS/CSS (development convenience)
@app.middleware("http")
async def _no_cache_static(request: Request, call_next):
    path = request.url.path
    if path.startswith("/static/") and path.endswith((".js", ".css", ".json")):
        response = await call_next(request)
        response.headers["Cache-Control"] = "no-cache, must-revalidate"
        return response
    return await call_next(request)


# Mount static files
static_path = Path(__file__).parent / "static"
if static_path.exists():
    app.mount("/static", StaticFiles(directory=str(static_path)), name="static")
    logger.info(f"Mounted static files from {static_path}")
else:
    logger.warning(f"Static files directory not found: {static_path}")

# Jinja2 templates
templates_path = Path(__file__).parent / "templates"
templates = Jinja2Templates(directory=str(templates_path))


@app.exception_handler(Exception)
async def global_exception_handler(request, exc):
    """Global exception handler for unhandled errors."""
    import uuid

    ref_id = uuid.uuid4().hex[:8]
    logger.error("Unhandled exception [ref=%s]: %s", ref_id, exc, exc_info=True)

    return JSONResponse(
        status_code=500,
        content={
            "error": "InternalServerError",
            "message": "Internal server error",
            "ref": ref_id,
        },
    )


@app.get("/")
async def root(request: Request):
    """Serve the web UI dashboard."""
    return templates.TemplateResponse(request, "index.html")


if __name__ == "__main__":
    import uvicorn

    uvicorn.run(
        "ledgrab.main:app",
        host=config.server.host,
        port=config.server.port,
        log_level=config.server.log_level.lower(),
        reload=False,  # Disabled due to watchfiles infinite reload loop
    )
