<?php
// app/services/MkvToolnixService.php

declare(strict_types=1);

namespace ScMedia\Services;

use ScMedia\Db\Db;
use ScMedia\Services\SettingsService;
use ScMedia\Services\ShellTool;

final class MkvToolnixService {
  private Db $db;
  private SettingsService $settings;
  private ?LogService $logger;
  private ShellTool $shell;
  private ?string $mkvmergePath = null;
  private ?string $mkvpropeditPath = null;

  public function __construct(Db $db, SettingsService $settings, ?LogService $logger = null, ?ShellTool $shell = null) {
    $this->db = $db;
    $this->settings = $settings;
    $this->logger = $logger;
    $this->shell = $shell ?? new ShellTool();
  }

  public function isAvailable(): bool {
    return $this->getMkvmergePath() !== null;
  }

  public function isMkvmergeAvailable(): bool {
    return $this->getMkvmergePath() !== null;
  }

  public function isMkvpropeditAvailable(): bool {
    return $this->getMkvpropeditPath() !== null;
  }

  public function readFileData(string $path): array {
    $path = rtrim($path, "/");
    if ($path === '') {
      return ['ok' => false, 'error' => 'Empty path'];
    }
    if (!is_file($path)) {
      return ['ok' => false, 'error' => 'File not found'];
    }
    if (!is_readable($path)) {
      return ['ok' => false, 'error' => 'File not readable'];
    }

    $stat = @stat($path);
    if (!is_array($stat)) {
      return ['ok' => false, 'error' => 'Cannot stat file'];
    }

    $size = (int)$stat['size'];
    $mtime = (int)$stat['mtime'];
    $inode = isset($stat['ino']) ? (int)$stat['ino'] : 0;

    $prev = $this->db->fetchOne("
      SELECT size_bytes, mtime, inode, info_json
      FROM media_file_meta
      WHERE abs_path = :p
      LIMIT 1
    ", [':p' => $path]);

    $changed = true;
    if ($prev) {
      $prevSize = (int)($prev['size_bytes'] ?? 0);
      $prevMtime = (int)($prev['mtime'] ?? 0);
      $prevInode = (int)($prev['inode'] ?? 0);
      if ($prevSize === $size && $prevMtime === $mtime && $prevInode === $inode) {
        $changed = false;
      }
    }

    if ($prev && !$changed) {
      $prevInfo = (string)($prev['info_json'] ?? '');
      if ($prevInfo === '' || $prevInfo === 'null') {
        $changed = true;
      }
    }

    $info = null;
    $fromCache = false;
    if ($changed) {
      $info = $this->probeMkvmerge($path);
      $this->upsertMeta($path, $size, $mtime, $inode, $info);
    } else if (is_array($prev)) {
      $cached = json_decode((string)($prev['info_json'] ?? ''), true);
      if (is_array($cached)) {
        $info = $cached;
        $fromCache = true;
      }
    }

    return [
      'ok' => true,
      'changed' => $changed,
      'from_cache' => $fromCache,
      'signature' => [
        'size_bytes' => $size,
        'mtime' => $mtime,
        'inode' => $inode,
      ],
      'file' => [
        'path' => $path,
        'name' => basename($path),
        'ext' => strtolower(pathinfo($path, PATHINFO_EXTENSION)),
      ],
      'mkvmerge' => [
        'available' => $this->isAvailable(),
        'data' => $info,
      ],
    ];
  }

  private function probeMkvmerge(string $path): ?array {
    if (!$this->isAvailable()) {
      return null;
    }

    $bin = $this->getMkvmergePath();
    if ($bin === null) return null;
    $cmd = escapeshellarg($bin) . ' -J ' . escapeshellarg($path) . ' 2>/dev/null';
    $this->logCmd('debug', $cmd);
    $json = $this->shell->exec($cmd);
    if (!is_string($json)) {
      return null;
    }

    $data = json_decode($json, true);
    if (is_array($data)) {
      $errors = $data['errors'] ?? [];
      if (is_array($errors) && count($errors) > 0) {
        $this->logger?->log('warn', 'mkvmerge reported errors', ['errors' => $errors]);
        return null;
      }

      $container = (string)($data['container']['type'] ?? '');
      $dur = $data['container']['properties']['duration'] ?? $data['container']['duration'] ?? null;
      $this->logger?->log('debug', 'mkvmerge parsed', [
        'container' => $container,
        'duration' => $dur,
      ]);
      return $data;
    }
    $this->logger?->log('warn', 'mkvmerge JSON parse failed');
    return null;
  }

  public function applyTrackEdits(string $path, array $rename, array $delete): array {
    if (!$this->isMkvpropeditAvailable()) {
      return ['ok' => false, 'error' => 'mkvpropedit not available'];
    }

    $args = [];
    foreach ($delete as $id) {
      $id = (int)$id;
      $args[] = '--delete';
      $args[] = 'track:' . $id;
    }

    foreach ($rename as $r) {
      $id = (int)($r['id'] ?? 0);
      $name = (string)($r['name'] ?? '');
      if ($id <= 0 || $name === '') continue;
      $args[] = '--edit';
      $args[] = 'track:' . $id;
      $args[] = '--set';
      $args[] = 'name=' . $name;
    }

    if (count($args) === 0) {
      return ['ok' => true, 'changed' => false];
    }

    $bin = $this->getMkvpropeditPath();
    if ($bin === null) {
      return ['ok' => false, 'error' => 'mkvpropedit not available'];
    }
    $cmd = escapeshellarg($bin) . ' ' . escapeshellarg($path);
    foreach ($args as $a) {
      $cmd .= ' ' . escapeshellarg($a);
    }
    $out = [];
    $code = 0;
    $this->logCmd('info', $this->shell->wrap($cmd));
    @exec($this->shell->wrap($cmd) . ' 2>&1', $out, $code);
    if ($code !== 0) {
      return ['ok' => false, 'error' => implode("\n", $out)];
    }

    return ['ok' => true, 'changed' => true];
  }

  public function convertToMkv(string $srcPath, string $destPath): array {
    if (!$this->isMkvmergeAvailable()) {
      return ['ok' => false, 'error' => 'mkvmerge not available'];
    }
    $bin = $this->getMkvmergePath();
    if ($bin === null) {
      return ['ok' => false, 'error' => 'mkvmerge not available'];
    }
    $cmd = escapeshellarg($bin) . ' -o ' . escapeshellarg($destPath) . ' ' . escapeshellarg($srcPath);
    $out = [];
    $code = 0;
    $this->logCmd('info', $this->shell->wrap($cmd));
    @exec($this->shell->wrap($cmd) . ' 2>&1', $out, $code);
    if ($code !== 0) {
      return ['ok' => false, 'error' => implode("\n", $out)];
    }
    return ['ok' => true];
  }

  private function upsertMeta(string $path, int $size, int $mtime, int $inode, ?array $info): void {
    $infoJson = $info === null ? null : json_encode($info, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    $this->db->exec("
      INSERT INTO media_file_meta (abs_path, size_bytes, mtime, inode, info_json, last_scanned_at)
      VALUES (:p, :s, :m, :i, :j, NOW())
      ON DUPLICATE KEY UPDATE
        size_bytes = VALUES(size_bytes),
        mtime = VALUES(mtime),
        inode = VALUES(inode),
        info_json = VALUES(info_json),
        last_scanned_at = NOW()
    ", [
      ':p' => $path,
      ':s' => $size,
      ':m' => $mtime,
      ':i' => $inode,
      ':j' => $infoJson,
    ]);
  }

  private function getMkvmergePath(): ?string {
    if ($this->mkvmergePath !== null) {
      return $this->mkvmergePath;
    }
    $all = $this->settings->getAll();
    $tools = $all['tools'] ?? [];
    $cfg = trim((string)($tools['mkvmerge_path'] ?? ''));
    if ($cfg !== '' && is_file($cfg)) {
      $this->mkvmergePath = $cfg;
      return $cfg;
    }
    $path = $this->shell->which('mkvmerge');
    $this->mkvmergePath = $path !== null ? $path : null;
    return $this->mkvmergePath;
  }

  private function getMkvpropeditPath(): ?string {
    if ($this->mkvpropeditPath !== null) {
      return $this->mkvpropeditPath;
    }
    $all = $this->settings->getAll();
    $tools = $all['tools'] ?? [];
    $cfg = trim((string)($tools['mkvpropedit_path'] ?? ''));
    if ($cfg !== '' && is_file($cfg)) {
      $this->mkvpropeditPath = $cfg;
      return $cfg;
    }
    $path = $this->shell->which('mkvpropedit');
    $this->mkvpropeditPath = $path !== null ? $path : null;
    return $this->mkvpropeditPath;
  }

  private function logCmd(string $level, string $cmd): void {
    if ($this->logger) {
      $this->logger->log($level, 'Exec: ' . $cmd, ['op' => 'mkvtoolnix']);
    }
  }

}
