"""Daemon core loop — multi-project discovery and periodic sweeps.

The daemon discovers all active projects from Postgres and runs sweeps
for each one on a configurable tick interval. It does not use an LLM —
it handles only mechanical orchestration tasks.
"""

from __future__ import annotations

import asyncio
import logging
from datetime import datetime, timezone
from typing import Any

import asyncpg
from redis.asyncio import Redis

from loom.bus import channels
from loom.config import LoomConfig
from loom.daemon.sweeps import sweep_project
from loom.graph.project import list_projects

log = logging.getLogger(__name__)


async def discover_projects(pool: asyncpg.Pool) -> list[dict]:
    """Discover all active (non-archived) projects from Postgres."""
    return await list_projects(pool, include_archived=False)


async def daemon_tick(
    pool: asyncpg.Pool,
    redis: Redis,
    config: LoomConfig,
    weft_pool: Any = None,
    weft_embedder: Any = None,
) -> dict:
    """Run a single daemon tick: discover projects, sweep each one.

    Returns a summary dict with tick stats.
    """
    # Increment tick counter stored in Redis (atomic, survives restarts)
    health_key = channels.daemon_health("global")
    tick_number = await redis.hincrby(health_key, "tick_count", 1)

    projects = await discover_projects(pool)
    swept = 0
    errors = 0
    results = []

    for proj in projects:
        project_id = str(proj["id"])
        result = await sweep_project(
            pool, redis, project_id, config,
            weft_pool=weft_pool, weft_embedder=weft_embedder,
        )
        results.append(result)
        if result.error:
            errors += 1
            log.error(
                "Project %s (%s) sweep error: %s",
                proj["name"], project_id, result.error,
            )
        else:
            swept += 1

    # Write health data to Redis (tick_count already incremented above)
    now = datetime.now(timezone.utc).isoformat()
    await redis.hset(health_key, mapping={
        "last_tick_at": now,
        "projects_discovered": str(len(projects)),
        "projects_swept": str(swept),
        "errors": str(errors),
    })

    summary = {
        "tick_number": tick_number,
        "projects_swept": swept,
        "errors": errors,
        "results": results,
    }

    if swept > 0 or errors > 0:
        log.info(
            "Daemon tick #%d: %d projects swept, %d errors",
            tick_number, swept, errors,
        )

    return summary


async def daemon_loop(
    pool: asyncpg.Pool,
    redis: Redis,
    config: LoomConfig,
    shutdown: asyncio.Event,
    weft_pool: Any = None,
    weft_embedder: Any = None,
) -> None:
    """Main daemon loop — runs ticks until shutdown is signalled.

    Args:
        pool: Postgres connection pool
        redis: Redis connection
        config: Loom configuration
        shutdown: asyncio.Event — set to signal graceful shutdown
        weft_pool: Optional Weft asyncpg pool for codebase_notes routing
        weft_embedder: Optional Weft embedder for codebase_notes routing
    """
    interval = config.daemon.tick_interval_seconds
    log.info("Daemon loop starting (tick_interval=%ds)", interval)

    while not shutdown.is_set():
        try:
            await daemon_tick(pool, redis, config, weft_pool, weft_embedder)
        except Exception:
            log.exception("Daemon tick failed (will retry next cycle)")

        # Wait for the interval, but wake up early if shutdown is signalled
        try:
            await asyncio.wait_for(shutdown.wait(), timeout=interval)
            # If we get here, shutdown was signalled
            break
        except asyncio.TimeoutError:
            # Normal: timeout means it's time for the next tick
            continue

    log.info("Daemon loop stopped.")
