<?php
// app/controllers/events.php

declare(strict_types=1);

use ScMedia\Http\Router;

final class EventsController extends BaseController {
  private \ScMedia\Services\LogService $logger;
  private \ScMedia\Services\AuthService $auth;

  private float $sourcesNextAt = 0.0;
  private float $sourcesInterval = 5.0;
  private array $sourcesLastMap = [];
  private float $sourcesSnapshotAt = 0.0;
  private float $jobsNextAt = 0.0;
  private float $jobsInterval = 1.0;
  private string $jobsLastHash = '';
  private float $jobsSnapshotAt = 0.0;
  private float $jobsSnapshotInterval = 5.0;
  private float $keepAliveNextAt = 0.0;
  private float $keepAliveInterval = 2.0;
  private float $authCheckNextAt = 0.0;
  private float $authCheckInterval = 0.5;
  private float $tickNextAt = 0.0;
  private float $tickInterval = 10.0;
  private float $snapshotInterval = 120.0;

  public function __construct(Container $container) {
    parent::__construct($container);
    $this->logger = $container->logger();
    $this->auth = $container->auth();
  }

  public static function register(Router $router, Container $container): void {
    $router->add('GET', '/api/events', function() use ($container) {
      (new self($container))->handle();
    });
  }

  public function handle(): void {
    $userId = $this->readUserId();
    if (!$userId) {
      http_response_code(401);
      header('Content-Type: text/plain; charset=utf-8');
      echo "Unauthorized";
      return;
    }

    $sessionToken = $this->auth->startSseSession($userId);
    $this->initSseHeaders();
    $this->sendReady();

    $this->logger->log('debug', 'SSE connected', [
      'user_id' => $userId,
      'session' => $sessionToken,
      'ip' => $_SERVER['REMOTE_ADDR'] ?? null,
      'ua' => $_SERVER['HTTP_USER_AGENT'] ?? null,
    ]);

    $start = time();
    $maxSeconds = 3600;

    $bg = $this->settings()->getAll()['background'] ?? [];
    $this->auth->setSseSessionTtl((int)($bg['sse_session_ttl_seconds'] ?? 20));

    $exitReason = 'max_seconds';
    while ((time() - $start) < $maxSeconds) {
      $now = microtime(true);
      try {
        if (connection_aborted()) {
          $this->logger->log('debug', 'SSE client aborted', [
            'user_id' => $userId,
            'session' => $sessionToken,
          ]);
          $exitReason = 'client_aborted';
          break;
        }
        $this->sendKeepalive($now);
        if (!$this->checkSession($now, $userId, $sessionToken)) {
          $exitReason = 'superseded';
          break;
        }
        $this->maybeUpdateTick($now);
        $this->sendJobs($now);
        $this->sendSources($now);
      } catch (Throwable $e) {
        $this->logger->log('error', 'SSE loop error', [
          'error' => $e->getMessage(),
          'file' => $e->getFile(),
          'line' => $e->getLine(),
        ]);
        $exitReason = 'loop_error';
        break;
      }
      usleep(300000);
    }

    $this->logger->log('debug', 'SSE closed', [
      'user_id' => $userId,
      'session' => $sessionToken,
      'reason' => $exitReason,
      'ip' => $_SERVER['REMOTE_ADDR'] ?? null,
      'ua' => $_SERVER['HTTP_USER_AGENT'] ?? null,
    ]);
  }

  // Read and validate SSE key.
  private function readUserId(): ?int {
    $key = isset($_COOKIE['scmedia_sse_key']) ? (string)$_COOKIE['scmedia_sse_key'] : '';
    $key = trim($key, "\"'");
    $key = urldecode($key);
    $userId = $this->auth->validateSseKey($key);
    if (!$userId) {
      $this->logger->log('warn', 'SSE auth failed', [
        'ip' => $_SERVER['REMOTE_ADDR'] ?? null,
        'has_cookie' => $key !== '',
      ]);
      return null;
    }
    return $userId;
  }

  // Initialize SSE headers and buffers.
  private function initSseHeaders(): void {
    @set_time_limit(0);
    @ignore_user_abort(true);
    header('Content-Type: text/event-stream; charset=utf-8');
    header('Cache-Control: no-cache');
    header('Connection: keep-alive');
    header('X-Accel-Buffering: no');

    @ini_set('output_buffering', 'off');
    @ini_set('zlib.output_compression', '0');
    while (ob_get_level() > 0) {
      ob_end_flush();
    }
    @ob_implicit_flush(true);
    @flush();
  }

  // Send initial SSE meta events.
  private function sendReady(): void {
    echo "retry: 5000\n";
    echo "event: ready\n";
    echo "data: {}\n\n";
    @flush();
  }

  // Emit keepalive comments.
  private function sendKeepalive(float $now): void {
    if ($now < $this->keepAliveNextAt) {
      return;
    }
    echo ": keepalive\n\n";
    $this->keepAliveNextAt = $now + $this->keepAliveInterval;
    if (ob_get_level() > 0) {
      @ob_flush();
    }
    @flush();
  }

  // Validate session and refresh heartbeat.
  private function checkSession(float $now, int $userId, string $sessionToken): bool {
    if ($now < $this->authCheckNextAt) {
      return true;
    }
    if (!$this->auth->isSseSessionActive($userId, $sessionToken)) {
      $this->logger->log('debug', 'SSE superseded', [
        'user_id' => $userId,
        'session' => $sessionToken,
        'ip' => $_SERVER['REMOTE_ADDR'] ?? null,
        'ua' => $_SERVER['HTTP_USER_AGENT'] ?? null,
      ]);
      return false;
    }
    $this->auth->touchSseSession($userId, $sessionToken);
    $this->authCheckNextAt = $now + $this->authCheckInterval;
    return true;
  }

  // Update tick/snapshot/keepalive intervals and emit tick.
  private function maybeUpdateTick(float $now): void {
    if ($now < $this->tickNextAt) {
      return;
    }
    $ui = $this->settings()->getAll()['ui'] ?? [];
    $bg = $this->settings()->getAll()['background'] ?? [];
    $tickSeconds = (int)($ui['sse_tick_seconds'] ?? 10);
    $snapSeconds = (int)($ui['sse_snapshot_seconds'] ?? 120);
    $keepAliveSeconds = (float)($ui['sse_keepalive_seconds'] ?? 2);
    $this->auth->setSseSessionTtl((int)($bg['sse_session_ttl_seconds'] ?? 20));
    $this->tickInterval = max(1.0, (float)$tickSeconds);
    $this->snapshotInterval = max(10.0, (float)$snapSeconds);
    $this->jobsSnapshotInterval = max(2.0, $this->tickInterval);
    $this->keepAliveInterval = max(1.0, min(5.0, $keepAliveSeconds));
    $this->tickNextAt = $now + $this->tickInterval;
    echo "event: tick\n";
    echo "data: " . json_encode(['ts' => time()], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n\n";
    if (ob_get_level() > 0) {
      @ob_flush();
    }
    @flush();
  }

  // Emit jobs payload (if changed/snapshot).
  private function sendJobs(float $now): void {
    if ($now < $this->jobsNextAt) {
      return;
    }
    $db = $this->db();
    $running = $db->fetchOne("SELECT COUNT(*) AS c FROM jobs WHERE status='running'");
    $queued = $db->fetchOne("SELECT COUNT(*) AS c FROM jobs WHERE status='queued'");
    $errors = $db->fetchOne("SELECT COUNT(*) AS c FROM jobs WHERE status='error'");
    $active = $db->fetchOne("
      SELECT id, type, title, status, progress_current, progress_total, progress_pct, cancel_requested
      FROM jobs
      WHERE status='running'
      ORDER BY started_at ASC
      LIMIT 1
    ");
    $bg = $this->settings()->getAll()['background'] ?? [];

    $activeList = $db->fetchAll("
      SELECT id, type, title, status, progress_current, progress_total, progress_pct, cancel_requested, started_at
      FROM jobs
      WHERE status='running'
      ORDER BY started_at ASC
      LIMIT 50
    ");
    $activeList = array_map(function($row) {
      return [
        'id' => (int)$row['id'],
        'type' => (string)$row['type'],
        'title' => (string)$row['title'],
        'status' => (string)$row['status'],
        'progress' => (int)($row['progress_current'] ?? 0),
        'progress_total' => (int)($row['progress_total'] ?? 0),
        'progress_pct' => (int)($row['progress_pct'] ?? 0),
        'cancel_requested' => !empty($row['cancel_requested']),
        'started_at' => $row['started_at'] ?? null,
      ];
    }, $activeList);

    $payload = [
      'paused' => !empty($bg['paused']),
      'running' => (int)($running['c'] ?? 0),
      'queued' => (int)($queued['c'] ?? 0),
      'errors' => (int)($errors['c'] ?? 0),
      'active' => $active ? [
        'id' => (int)$active['id'],
        'type' => (string)$active['type'],
        'title' => (string)$active['title'],
        'status' => (string)$active['status'],
        'progress' => (int)($active['progress_current'] ?? 0),
        'progress_total' => (int)($active['progress_total'] ?? 0),
        'progress_pct' => (int)($active['progress_pct'] ?? 0),
        'cancel_requested' => !empty($active['cancel_requested']),
      ] : null,
      'active_list' => $activeList,
    ];

    $hash = md5(json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
    $changed = $hash !== $this->jobsLastHash;
    $isSnapshot = $now >= $this->jobsSnapshotAt || $this->jobsSnapshotAt === 0.0;
    if ($changed || $isSnapshot) {
      $this->jobsLastHash = $hash;
      if ($isSnapshot) {
        $this->jobsSnapshotAt = $now + $this->jobsSnapshotInterval;
      }
      echo "event: jobs\n";
      echo "data: " . json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n\n";
    }
    $this->jobsNextAt = $now + $this->jobsInterval;
    if (ob_get_level() > 0) {
      @ob_flush();
    }
    @flush();
  }

  // Emit sources payload and adjust interval.
  private function sendSources(float $now): void {
    if ($now < $this->sourcesNextAt) {
      return;
    }
    $sourcesService = $this->services()['sources'] ?? null;
    if (!$sourcesService instanceof \ScMedia\Services\SourcesService) {
      return;
    }
    $elapsed = 0.0;
    $items = [];
    try {
      $t0 = microtime(true);
      $items = $sourcesService->list();
      $elapsed = microtime(true) - $t0;
    } catch (Throwable $e) {
      $this->logger->log('warn', 'SSE sources failed', ['error' => $e->getMessage()]);
    }

    $map = [];
    foreach ($items as $it) {
      $key = (string)($it['source'] ?? '') . ':' . (string)($it['id'] ?? '');
      if ($key === ':') continue;
      $map[$key] = $it;
    }

    $changed = [];
    foreach ($map as $key => $it) {
      if (!isset($this->sourcesLastMap[$key])) {
        $changed[] = $it;
        continue;
      }
      $prev = $this->sourcesLastMap[$key];
      if (($prev['status'] ?? null) !== ($it['status'] ?? null)
        || ($prev['percent_done'] ?? null) !== ($it['percent_done'] ?? null)
        || ($prev['size_bytes'] ?? null) !== ($it['size_bytes'] ?? null)
        || ($prev['name'] ?? null) !== ($it['name'] ?? null)
        || ($prev['content_type'] ?? null) !== ($it['content_type'] ?? null)
        || ($prev['file_count'] ?? null) !== ($it['file_count'] ?? null)) {
        $changed[] = $it;
      }
    }
    $removed = array_values(array_diff(array_keys($this->sourcesLastMap), array_keys($map)));

    $isSnapshot = $now >= $this->sourcesSnapshotAt || $this->sourcesSnapshotAt === 0.0;
    if ($isSnapshot) {
      $this->sourcesSnapshotAt = $now + $this->snapshotInterval;
    }

    if ($isSnapshot || count($changed) > 0 || count($removed) > 0) {
      echo "event: sources\n";
      echo "data: " . json_encode([
        'items' => $isSnapshot ? array_values($items) : $changed,
        'removed' => $removed,
        'snapshot' => $isSnapshot,
        'ts' => time(),
      ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n\n";
    }

    $this->sourcesLastMap = $map;
    $cfg = $this->settings()->getAll()['sources']['transmission'] ?? [];
    $host = (string)($cfg['host'] ?? '');
    $isLocal = in_array($host, ['127.0.0.1', 'localhost', '::1'], true);
    $base = $isLocal ? 2.5 : 6.0;
    $penalty = max(0.0, $elapsed - 0.4) * 2.0;
    $this->sourcesInterval = min(12.0, max(1.0, $base + $penalty));
    $this->sourcesNextAt = $now + $this->sourcesInterval;
    if (ob_get_level() > 0) {
      @ob_flush();
    }
    @flush();
  }
}
