"""Main daemon loop: poll → collect → push → save state → sleep."""

from __future__ import annotations

import json
import logging
import os
import signal
import socket
import sys
import time
import urllib.error
import urllib.parse
import urllib.request

from qc_trace import __version__
from qc_trace.daemon.collector import collect_file
from qc_trace.daemon.config import DaemonConfig
from qc_trace.daemon.push_status import record_push, record_update_check
from qc_trace.daemon.pusher import Pusher
from qc_trace.daemon.state import StateManager
from qc_trace.daemon.watcher import FileWatcher
from qc_trace.utils.repo_resolver import resolve_global_identity

logger = logging.getLogger("quickcall")

_UPDATE_CHECK_INTERVAL = 6 * 3600  # 6 hours


class Daemon:
    """The quickcall daemon process."""

    def __init__(self, config: DaemonConfig | None = None) -> None:
        self.config = config or DaemonConfig()
        self.state_mgr = StateManager(state_file=self.config.state_file)
        self.watcher = FileWatcher(self.config, self.state_mgr)
        self.pusher = Pusher(config=self.config)
        self._shutdown = False
        self.device_name: str | None = None
        self.global_email: str | None = None
        self.global_name: str | None = None
        self._last_update_check: float = 0.0

    def run(self) -> None:
        """Main daemon loop."""
        self._setup_signals()
        self._write_pid()
        self.state_mgr.load()
        self._reconcile()
        self.state_mgr.prune_missing_files()
        self.device_name = socket.gethostname()
        self.global_email, self.global_name = resolve_global_identity()

        logger.info(
            "quickcall daemon started v%s (poll_interval=%.1fs, ingest=%s)",
            __version__,
            self.config.poll_interval,
            self.config.ingest_url,
        )

        try:
            while not self._shutdown:
                self._poll_cycle()
                self._check_for_update()
                self._sleep(self.config.poll_interval)
        except KeyboardInterrupt:
            pass
        finally:
            self._cleanup()

    def _reconcile(self) -> None:
        """Compare local state against server and reset files where server is behind."""
        try:
            server_state = self._fetch_server_state()
        except Exception as e:
            logger.warning("Reconciliation skipped — server unreachable: %s", e)
            return

        reset_count = 0
        for file_path, local in self.state_mgr.all_states().items():
            server = server_state.get(file_path)

            if server is None and local.last_line_processed > 0:
                # Server has nothing for this file but daemon thinks it's processed
                self.state_mgr.reset_state(file_path)
                reset_count += 1
            elif server is None and local.content_hash:
                # Hash-based file with no server data
                self.state_mgr.reset_state(file_path)
                reset_count += 1
            elif server is not None and local.source in ("claude_code", "codex_cli"):
                # JSONL sources: prefer last_line_read (daemon's actual position),
                # fall back to max_line (messages) for backwards compatibility
                server_line = server.get("last_line_read") or server["max_line"]
                if server_line < local.last_line_processed:
                    local.last_line_processed = server_line
                    self.state_mgr.set_state(local)
                    reset_count += 1
            elif server is not None and local.source in ("gemini_cli", "cursor"):
                # Hash-based sources: if server has 0 messages, force re-process
                if server["message_count"] == 0:
                    self.state_mgr.reset_state(file_path)
                    reset_count += 1

        if reset_count > 0:
            logger.info(
                "Reconciliation: reset %d file(s) for re-processing", reset_count
            )
            self.state_mgr.save()

    def _fetch_server_state(self) -> dict[str, dict]:
        """GET /api/sync from the ingest server, return dict keyed by file_path."""
        url = urllib.parse.urljoin(self.config.ingest_url, "/api/sync")
        req = urllib.request.Request(url, method="GET")
        with urllib.request.urlopen(req, timeout=10) as resp:
            rows = json.loads(resp.read())
        return {row["raw_file_path"]: row for row in rows}

    def _poll_cycle(self) -> None:
        """One poll-collect-push cycle."""
        changed_files = self.watcher.get_changed_files()

        for changed in changed_files:
            if self._shutdown:
                break

            existing_state = self.state_mgr.get_state(changed.path)

            try:
                result = collect_file(
                    changed,
                    existing_state,
                    device_name=self.device_name,
                    global_email=self.global_email,
                    global_name=self.global_name,
                    org=self.config.org,
                )
            except Exception as e:
                logger.error("Failed to collect %s: %s", changed.path, e)
                # Increment retry count
                if existing_state:
                    existing_state.retry_count += 1
                    existing_state.last_error = str(e)
                    existing_state.last_mtime = changed.mtime
                    existing_state.last_size = changed.size
                    self.state_mgr.set_state(existing_state)
                continue

            if result.messages:
                # Push in batches — only advance state if all pushes succeed
                all_pushed = True
                for i in range(0, len(result.messages), self.config.batch_size):
                    batch = result.messages[i : i + self.config.batch_size]
                    if not self.pusher.push(batch):
                        all_pushed = False
                        break

                if not all_pushed:
                    logger.warning(
                        "Push failed for %s — state not advanced", changed.path
                    )
                    continue

                record_push(result.new_state.source, len(result.messages))

            self.state_mgr.set_state(result.new_state)

            # Report read progress so server tracks actual daemon position
            self.pusher.report_progress(
                file_path=changed.path,
                source=result.new_state.source,
                last_line_read=result.new_state.last_line_processed,
                content_hash=result.new_state.content_hash or None,
            )

        self.state_mgr.save()

    def _check_for_update(self) -> None:
        """Periodically check PyPI for a newer version and exit to trigger restart."""
        now = time.time()
        if now - self._last_update_check < _UPDATE_CHECK_INTERVAL:
            return
        self._last_update_check = now

        newer = None
        try:
            url = "https://pypi.org/pypi/qc-trace/json"
            with urllib.request.urlopen(url, timeout=10) as resp:
                data = json.loads(resp.read())
                latest = data["info"]["version"]
                if latest != __version__:
                    newer = latest
        except Exception:
            pass  # network issues must never crash the daemon

        record_update_check(__version__, newer)

        if newer:
            logger.info("Update available: %s → %s, restarting...", __version__, newer)
            self._cleanup()
            sys.exit(0)  # systemd/launchd restarts → uvx fetches new version

    def _sleep(self, duration: float) -> None:
        """Sleep that respects shutdown signal."""
        end = time.monotonic() + duration
        while not self._shutdown and time.monotonic() < end:
            time.sleep(max(0, min(0.5, end - time.monotonic())))

    def _setup_signals(self) -> None:
        """Register signal handlers for graceful shutdown."""
        signal.signal(signal.SIGTERM, self._handle_signal)
        signal.signal(signal.SIGINT, self._handle_signal)

    def _handle_signal(self, signum: int, frame: object) -> None:
        logger.info("Received signal %d, shutting down...", signum)
        self._shutdown = True

    def _write_pid(self) -> None:
        """Write PID file, warning if a stale PID file exists."""
        self.config.base_dir.mkdir(parents=True, exist_ok=True)
        if self.config.pid_file.exists():
            try:
                old_pid = int(self.config.pid_file.read_text().strip())
                os.kill(old_pid, 0)  # check if process exists
                logger.warning(
                    "Another daemon may be running (PID %d)", old_pid
                )
            except (ValueError, ProcessLookupError, PermissionError):
                logger.info("Removing stale PID file")
        self.config.pid_file.write_text(str(os.getpid()))

    def _cleanup(self) -> None:
        """Clean up on shutdown."""
        logger.info("quickcall daemon shutting down")
        self.state_mgr.save()
        try:
            self.config.pid_file.unlink(missing_ok=True)
        except OSError:
            pass
