from __future__ import annotations

import asyncio
import json
import logging
import sys
import time
from datetime import datetime

import structlog
import uvicorn
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from pydantic import ValidationError

from .api import app, register_checks, register_ws_monitor
from .checks.automation_failures import AutomationFailuresCheck
from .checks.heartbeat import HeartbeatCheck
from .checks.system_health import SystemHealthCheck
from .checks.unavailable_entities import UnavailableEntitiesCheck
from .checks.updates_available import UpdatesAvailableCheck, UpdatesDigestCheck
from .config import Settings
from .event_emitter import EventEmitter
from .ha_client import HAClient, make_session
from .monitors import WebSocketMonitor
from .storage import Storage

_log = structlog.get_logger()


def _configure_structlog(log_level: str) -> None:
    structlog.configure(
        processors=[
            structlog.processors.add_log_level,
            structlog.processors.TimeStamper(fmt="iso"),
            structlog.processors.StackInfoRenderer(),
            structlog.processors.format_exc_info,
            structlog.processors.JSONRenderer(),
        ],
        logger_factory=structlog.PrintLoggerFactory(),
    )
    logging.basicConfig(level=getattr(logging, log_level.upper(), logging.INFO))


async def _run_check_and_emit(
    check, emitter: EventEmitter, storage: Storage
) -> None:
    """Run a check, emit events for each result, and record to check_history."""
    try:
        results = await check.run()
        healthy = not any(r.event_type for r in results)
        summary = f"{len(results)} issue(s)" if results else "ok"

        await storage.record_check(
            check_name=check.name,
            ran_at=time.time(),
            healthy=healthy,
            message=summary,
            payload=json.dumps([r.model_dump() for r in results]),
        )

        for result in results:
            if result.event_type:
                emitter.emit(
                    event_type=result.event_type,
                    severity=result.severity.value,
                    service="homeassistant",
                    message=result.message,
                    payload=result.payload,
                )
                _log.warning(
                    "check_unhealthy",
                    check=check.name,
                    ha_event=result.event_type,
                    msg=result.message,
                )

        if healthy:
            _log.info("check_ok", check=check.name)

    except Exception as exc:
        _log.error("check_error", check=check.name, error=str(exc), exc_info=True)


async def run(settings: Settings) -> None:
    _configure_structlog(settings.log_level)
    _log.info(
        "ha_diag_agent_starting",
        node=settings.node_name,
        location=settings.location_tag,
        ha_url=settings.ha_url,
        heartbeat_interval=settings.check_interval,
        unavailable_interval=settings.check_interval_unavailable,
    )

    storage = Storage(settings.data_dir / "ha_diag.db")
    await storage.open()

    emitter = EventEmitter(settings.events_dir, settings.node_name, settings.location_tag)

    # Shared session — created once at startup, closed on shutdown
    session = make_session(settings.ha_token, settings.ha_timeout)
    ha_client = HAClient(
        settings.ha_url, session,
        entity_registry_cache_ttl=settings.entity_registry_cache_ttl,
    )

    heartbeat = HeartbeatCheck(ha_client)
    unavailable = UnavailableEntitiesCheck(ha_client, storage, settings)
    system_health = SystemHealthCheck(ha_client, storage, settings)
    automation_failures = AutomationFailuresCheck(ha_client, storage, settings)
    updates_daily = UpdatesAvailableCheck(ha_client, storage, settings)
    updates_digest = UpdatesDigestCheck(ha_client, storage, settings)

    all_checks = [heartbeat, unavailable, system_health, automation_failures,
                  updates_daily, updates_digest]
    register_checks(all_checks, settings.node_name, settings.location_tag)

    ws_monitor = WebSocketMonitor(
        ha_url=settings.ha_url,
        token=settings.ha_token,
        settings=settings,
        emitter=emitter,
        session=session,
    )
    register_ws_monitor(ws_monitor)

    scheduler = AsyncIOScheduler()
    scheduler.add_job(
        _run_check_and_emit, "interval",
        seconds=settings.check_interval,
        args=[heartbeat, emitter, storage],
        id="check_heartbeat",
        next_run_time=datetime.now(),
    )
    scheduler.add_job(
        _run_check_and_emit, "interval",
        seconds=settings.check_interval_unavailable,
        args=[unavailable, emitter, storage],
        id="check_unavailable_entities",
        next_run_time=datetime.now(),
    )
    scheduler.add_job(
        _run_check_and_emit, "interval",
        seconds=settings.system_health_check_interval,
        args=[system_health, emitter, storage],
        id="check_system_health",
        next_run_time=datetime.now(),
    )
    scheduler.add_job(
        _run_check_and_emit, "interval",
        seconds=settings.automation_check_interval,
        args=[automation_failures, emitter, storage],
        id="check_automation_failures",
        next_run_time=datetime.now(),
    )
    scheduler.add_job(
        _run_check_and_emit, "cron",
        hour=settings.updates_check_hour,
        minute=settings.updates_check_minute,
        args=[updates_daily, emitter, storage],
        id="check_updates_available",
    )
    scheduler.add_job(
        _run_check_and_emit, "cron",
        day_of_week="sun",
        hour=settings.updates_check_hour,
        minute=settings.updates_check_minute,
        args=[updates_digest, emitter, storage],
        id="check_updates_digest",
    )
    scheduler.start()
    _log.info(
        "scheduler_started",
        checks=[c.name for c in all_checks],
        heartbeat_interval=settings.check_interval,
        unavailable_interval=settings.check_interval_unavailable,
        system_health_interval=settings.system_health_check_interval,
        automation_interval=settings.automation_check_interval,
        updates_hour=settings.updates_check_hour,
    )

    await ws_monitor.start()

    config = uvicorn.Config(
        app,
        host="0.0.0.0",
        port=settings.port,
        log_level=settings.log_level.lower(),
    )
    server = uvicorn.Server(config)
    try:
        await server.serve()
    finally:
        await ws_monitor.stop()
        scheduler.shutdown(wait=False)
        await storage.close()
        await session.close()


def main() -> None:
    try:
        settings = Settings.load()
    except ValidationError as exc:
        # Fail fast and loud rather than run mis-identified. The most important
        # case is node_name="unknown"/empty, which would emit evt-unknown-*
        # poison files into another node's events dir (see d5139c9).
        print(
            "FATAL: refusing to start ha-diag-agent — invalid configuration:\n"
            f"{exc}",
            file=sys.stderr,
        )
        raise SystemExit(1)
    asyncio.run(run(settings))


if __name__ == "__main__":
    main()
