<?php

declare(strict_types=1);

namespace PpPhp\Core\Replication;

use PpPhp\Core\DB;
use PpPhp\Core\Licensing\LicenseConfig;
use PpPhp\Core\Licensing\LicenseMirror;
use PpPhp\Core\Licensing\LicenseSync;
use PpPhp\Core\Licensing\ServerRole;

/**
 * One place that assembles the complete, role-aware picture of replication for
 * the owner dashboard: who is paired to whom, when each server last exchanged
 * data, how fresh each channel is, what is being protected (counts), and what a
 * backup physically holds on disk. Read-only and defensive — every source is
 * wrapped so a missing table or unpaired state degrades to zeros, never a 500.
 */
final class ReplicationReport
{
    /** Assemble the full report. Safe to call on a main, a backup, or an unpaired install. */
    public static function gather(?array $server = null): array
    {
        $server = $server ?? $_SERVER;
        $now    = time();
        $role   = ServerRole::isBackup() ? 'backup' : 'main';

        $self = '';
        if (class_exists(\PpPhp\Core\CoreConfig::class)) {
            try { $self = (string) \PpPhp\Core\CoreConfig::detectSelfUrl($server); } catch (\Throwable $e) {}
        }

        $report = [
            'now'        => $now,
            'role'       => $role,
            'selfUrl'    => $self,
            'selfLabel'  => self::labelFor($self),
            'controlInterval' => LicenseMirror::SYNC_INTERVAL,
            'estate'     => self::estateCounts(),
            'main'       => null,
            'backup'     => null,
        ];

        if ($role === 'main') {
            $report['main'] = self::mainView($now);
        } else {
            $report['backup'] = self::backupView($now);
        }
        return $report;
    }

    // ── MAIN: the outgoing mesh ────────────────────────────────────────

    private static function mainView(int $now): array
    {
        $primary = '';
        $mirrors = [];
        $labels  = [];
        try { $primary = LicenseConfig::primary(); } catch (\Throwable $e) {}
        try { $mirrors = LicenseConfig::distributableBackups(); } catch (\Throwable $e) {}
        try { $labels  = LicenseConfig::labels(); } catch (\Throwable $e) {}

        $peers = [];
        try {
            foreach ((new LicenseSync())->peers() as $p) {
                $lastSync = self::ts($p['last_sync_at'] ?? null);
                $paired   = self::ts($p['paired_at'] ?? null);
                $status   = (string) ($p['status'] ?? '');
                $url      = (string) ($p['url'] ?? '');
                $age      = $lastSync > 0 ? ($now - $lastSync) : null;
                $peers[] = [
                    'id'        => (int) ($p['id'] ?? 0),
                    'label'     => (string) ($p['label'] ?? '') ?: ($labels[$url] ?? ''),
                    'host'      => (string) ($p['host'] ?? ''),
                    'url'       => $url,
                    'status'    => $status,
                    'lastIp'    => (string) ($p['last_ip'] ?? ''),
                    'lastSync'  => $lastSync,
                    'pairedAt'  => $paired,
                    'ageSeconds'=> $age,
                    // Healthy = active AND heard from within 2× the control-plane interval.
                    'health'    => self::peerHealth($status, $age),
                ];
            }
        } catch (\Throwable $e) {}

        $active = 0; $healthy = 0; $oldest = null;
        foreach ($peers as $pr) {
            if ($pr['status'] === 'active') $active++;
            if ($pr['health'] === 'ok') $healthy++;
            if ($pr['ageSeconds'] !== null) $oldest = $oldest === null ? $pr['ageSeconds'] : max($oldest, $pr['ageSeconds']);
        }

        return [
            'primary'      => $primary,
            'mirrors'      => $mirrors,
            'peers'        => $peers,
            'peerCount'    => count($peers),
            'activeCount'  => $active,
            'healthyCount' => $healthy,
            'oldestLag'    => $oldest,        // seconds since the least-recently-seen backup checked in
        ];
    }

    // ── BACKUP: the incoming sync health ───────────────────────────────

    private static function backupView(int $now): array
    {
        $mirror = [];
        try { $mirror = (new LicenseMirror())->status(); } catch (\Throwable $e) {}

        $clone = [];
        try { $clone = \PpPhp\Core\Databases\CloneSchedule::status(constant('BASE_PATH')); } catch (\Throwable $e) {}
        $interval = (int) ($clone['interval'] ?? 21600);
        $lastRun  = (int) ($clone['lastRun'] ?? 0);
        $nextDue  = $lastRun > 0 ? max(0, ($lastRun + $interval) - $now) : 0;

        // File-replication holdings (from the FileReplicator state + disk).
        $files = self::fileHoldings();

        return [
            'paired'        => (bool) ($mirror['paired'] ?? false),
            'mainUrl'       => (string) ($mirror['mainUrl'] ?? ''),
            'mainLabel'     => self::labelFor((string) ($mirror['mainUrl'] ?? '')),
            'keyId'         => (string) ($mirror['keyId'] ?? ''),
            'pairedAt'      => (int) ($mirror['pairedAt'] ?? 0),
            'controlLastSync'=> (int) ($mirror['lastSync'] ?? 0),
            'controlError'  => (string) ($mirror['lastError'] ?? ''),
            'controlInterval'=> (int) ($mirror['interval'] ?? LicenseMirror::SYNC_INTERVAL),
            // Data/file clone channel
            'cloneEnabled'  => (bool) ($clone['enabled'] ?? true),
            'cloneAuto'     => true,   // self-driving (no cron)
            'cloneInterval' => $interval,
            'cloneLastRun'  => $lastRun,
            'cloneNextDue'  => $nextDue,
            'cloneLastResult'=> (string) ($clone['lastResult'] ?? ''),
            'cloneLastOk'   => (bool) ($clone['lastOk'] ?? false),
            'cloneDue'      => (bool) ($clone['due'] ?? false),
            'cloneStale'    => (bool) ($clone['stale'] ?? false),
            'files'         => $files,
        ];
    }

    /** What this backup physically holds + what the file replicator is tracking. */
    private static function fileHoldings(): array
    {
        $base = rtrim((string) constant('BASE_PATH'), '/');

        // Tracked (snapshot markers) from the replicator state.
        $tracked = [];
        $stateFile = $base . '/storage/replication/files.json';
        if (is_file($stateFile)) {
            $s = json_decode((string) @file_get_contents($stateFile), true);
            if (is_array($s)) {
                foreach ($s as $set => $map) {
                    if (is_array($map)) $tracked[(string) $set] = count($map);
                }
            }
        }

        // On-disk holdings.
        $vdbFiles = 0; $vdbBytes = 0;
        foreach (glob($base . '/storage/vdb/*.sqlite') ?: [] as $f) {
            $vdbFiles++; $vdbBytes += (int) @filesize($f);
        }
        $projCount = 0; $projBytes = 0;
        $proot = $base . '/projects';
        if (is_dir($proot)) {
            foreach (glob($proot . '/*', GLOB_ONLYDIR) ?: [] as $ownerDir) {
                foreach (glob($ownerDir . '/*', GLOB_ONLYDIR) ?: [] as $projDir) {
                    if (is_file($projDir . '/config/database.php') || is_dir($projDir . '/config')) {
                        $projCount++;
                        $projBytes += self::dirBytes($projDir);
                    }
                }
            }
        }

        return [
            'trackedVdb'   => (int) ($tracked['vdb'] ?? 0),
            'trackedProj'  => (int) ($tracked['projects'] ?? 0),
            'vdbFiles'     => $vdbFiles,
            'vdbBytes'     => $vdbBytes,
            'projectCount' => $projCount,
            'projectBytes' => $projBytes,
        ];
    }

    // ── Shared: what's being protected (counts) ────────────────────────

    private static function estateCounts(): array
    {
        return [
            'licences'   => self::count('pp_licenses'),
            'instances'  => self::count('pp_license_instances'),
            'users'      => self::count('users'),
            'projects'   => self::count('projects'),
            'virtualDbs' => self::count('pp_virtual_databases'),
            'managedDbs' => self::count('pp_managed_databases'),
        ];
    }

    private static function count(string $table): int
    {
        try {
            $r = DB::FetchAll('SELECT COUNT(*) AS c FROM `' . $table . '`');
            return (int) ($r[0]['c'] ?? 0);
        } catch (\Throwable $e) {
            return 0;
        }
    }

    // ── helpers ────────────────────────────────────────────────────────

    private static function peerHealth(string $status, ?int $age): string
    {
        if ($status !== 'active') {
            return $status === 'suspended' ? 'suspended' : ($status === 'pending' ? 'pending' : 'revoked');
        }
        if ($age === null) return 'new';
        if ($age <= LicenseMirror::SYNC_INTERVAL * 2) return 'ok';      // within 30 min
        if ($age <= 86400) return 'lagging';                            // within a day
        return 'stale';
    }

    private static function labelFor(string $url): string
    {
        if ($url === '') return '';
        try {
            $labels = LicenseConfig::labels();
            foreach ($labels as $u => $l) {
                if (rtrim((string) $u, '/') === rtrim($url, '/') && trim((string) $l) !== '') {
                    return (string) $l;
                }
            }
        } catch (\Throwable $e) {}
        return '';
    }

    /** Parse a DB datetime (UTC) to a unix ts, or 0. */
    private static function ts(mixed $v): int
    {
        if ($v === null || $v === '' || $v === '0000-00-00 00:00:00') return 0;
        if (is_int($v)) return $v;
        $t = strtotime((string) $v . ' UTC');
        return $t !== false ? $t : 0;
    }

    private static function dirBytes(string $dir): int
    {
        $bytes = 0; $seen = 0;
        try {
            $it = new \RecursiveIteratorIterator(
                new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS),
                \RecursiveIteratorIterator::LEAVES_ONLY
            );
            foreach ($it as $f) {
                if (++$seen > 50000) break;
                if ($f->isFile()) $bytes += (int) $f->getSize();
            }
        } catch (\Throwable $e) {}
        return $bytes;
    }

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

    /** Relative "3 minutes ago" / "in 2 hours". */
    public static function ago(int $ts, ?int $now = null): string
    {
        if ($ts <= 0) return 'never';
        $now = $now ?? time();
        $d = $now - $ts;
        $future = $d < 0;
        $d = abs($d);
        $s = match (true) {
            $d < 45        => 'just now',
            $d < 90        => 'a minute',
            $d < 3600      => round($d / 60) . ' minutes',
            $d < 5400      => 'an hour',
            $d < 86400     => round($d / 3600) . ' hours',
            $d < 172800    => 'a day',
            $d < 2592000   => round($d / 86400) . ' days',
            default        => round($d / 2592000) . ' months',
        };
        if ($s === 'just now') return $s;
        return $future ? "in $s" : "$s ago";
    }
}
