<?php

declare(strict_types=1);

namespace PpPhp\Core\Deploy;

use PpPhp\Core\DB;

/**
 * Records each project build in `deploy_builds`, including a path→hash manifest
 * of the output. This gives the Deploy client an authoritative history of
 * builds and lets it show what changed between the last two builds.
 */
class BuildRecorder
{
    /**
     * Snapshot a completed build. Best-effort: never throws into the caller.
     *
     * @return int the new build id (0 on failure)
     */
    public static function record(int $projectId, string $outputDir, float $duration = 0, ?int $userId = null): int
    {
        if ($projectId <= 0 || $outputDir === '' || !is_dir($outputDir)) {
            return 0;
        }
        try {
            $manifest = self::manifest($outputDir);
            $bytes = array_sum(array_map(static fn($e) => $e['size'], $manifest));

            DeploySchema::ensureSchema();
            DB::Init();
            DB::SetConnection();

            return (int) DB::Insert('deploy_builds', [
                'project_id' => $projectId,
                'files'      => count($manifest),
                'size_bytes' => $bytes,
                'duration'   => $duration,
                'output_dir' => mb_substr($outputDir, 0, 490),
                'manifest'   => json_encode($manifest, JSON_UNESCAPED_SLASHES),
                'created_by' => $userId,
            ]);
        } catch (\Throwable $e) {
            return 0;
        }
    }

    /** The most recent build row for a project, or null. */
    public static function latest(int $projectId): ?array
    {
        try {
            DeploySchema::ensureSchema();
            DB::Init();
            DB::SetConnection();
            $rows = DB::FetchAll(
                'SELECT id, files, size_bytes, duration, output_dir, created_at FROM `deploy_builds` WHERE `project_id` = ? ORDER BY id DESC LIMIT 1',
                [$projectId]
            );
            return $rows[0] ?? null;
        } catch (\Throwable $e) {
            return null;
        }
    }

    /**
     * The most recent build, normalized for display. This is the single source
     * of truth every module (Output, Finished, Deploy) reads so the build
     * figures are identical everywhere. Returns null if no build is recorded.
     *
     * @return array{time:?string,files:int,duration:float,bytes:int,size:string,output_dir:string,count:int}|null
     */
    public static function latestInfo(int $projectId): ?array
    {
        $r = self::latest($projectId);
        if ($r === null) {
            return null;
        }
        $bytes = (int) ($r['size_bytes'] ?? 0);
        return [
            'time'       => $r['created_at'] ?? null,
            'files'      => (int) ($r['files'] ?? 0),
            'duration'   => (float) ($r['duration'] ?? 0),
            'bytes'      => $bytes,
            'size'       => self::humanSize($bytes),
            'output_dir' => (string) ($r['output_dir'] ?? ''),
            'count'      => self::count($projectId),
        ];
    }

    /** Human-readable byte size (shared formatting so all modules agree). */
    public static function humanSize(int $bytes): string
    {
        if ($bytes <= 0) {
            return '0 B';
        }
        $units = ['B', 'KB', 'MB', 'GB', 'TB'];
        $i = (int) floor(log($bytes, 1024));
        $i = max(0, min($i, count($units) - 1));
        return round($bytes / (1024 ** $i), $i ? 1 : 0) . ' ' . $units[$i];
    }

    /** Total number of builds recorded for a project. */
    public static function count(int $projectId): int
    {
        try {
            DeploySchema::ensureSchema();
            DB::Init();
            DB::SetConnection();
            return (int) DB::FetchOne('SELECT COUNT(*) FROM `deploy_builds` WHERE `project_id` = ?', [$projectId]);
        } catch (\Throwable $e) {
            return 0;
        }
    }

    /**
     * Compare the two most recent builds and return the relative paths that
     * were added or changed in the latest one.
     *
     * @return string[]
     */
    public static function changedInLatest(int $projectId): array
    {
        try {
            DeploySchema::ensureSchema();
            DB::Init();
            DB::SetConnection();
            $rows = DB::FetchAll(
                'SELECT manifest FROM `deploy_builds` WHERE `project_id` = ? ORDER BY id DESC LIMIT 2',
                [$projectId]
            ) ?: [];
            if (count($rows) < 2) {
                return [];
            }
            $new = json_decode((string) $rows[0]['manifest'], true) ?: [];
            $old = json_decode((string) $rows[1]['manifest'], true) ?: [];
            $changed = [];
            foreach ($new as $path => $e) {
                if (!isset($old[$path]) || ($old[$path]['hash'] ?? '') !== ($e['hash'] ?? '')) {
                    $changed[] = $path;
                }
            }
            return $changed;
        } catch (\Throwable $e) {
            return [];
        }
    }

    /** @return array<string,array{hash:string,size:int}> */
    private static function manifest(string $outputDir): array
    {
        $outputDir = rtrim($outputDir, '/');
        $skip = ['.git', '.DS_Store', 'node_modules'];
        $out = [];
        $iter = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($outputDir, \RecursiveDirectoryIterator::SKIP_DOTS),
            \RecursiveIteratorIterator::LEAVES_ONLY
        );
        $rootLen = strlen($outputDir) + 1;
        foreach ($iter as $file) {
            if (!$file->isFile()) {
                continue;
            }
            $rel = str_replace('\\', '/', substr($file->getPathname(), $rootLen));
            foreach ($skip as $s) {
                if (str_contains('/' . $rel . '/', '/' . $s . '/')) {
                    continue 2;
                }
            }
            $out[$rel] = ['hash' => (string) @md5_file($file->getPathname()), 'size' => (int) $file->getSize()];
        }
        return $out;
    }
}
