<?php

declare(strict_types=1);

namespace Lab;

use PDO;

final class License
{
    private const ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
    private const GRACE_DAYS = 14;

    public function __construct(
        private array $config,
        private PDO $pdo,
    ) {}

    public static function isKiosk(): bool
    {
        if (getenv('LAB_KIOSK') === '1') {
            return true;
        }

        return ($_SERVER['LAB_KIOSK'] ?? $_ENV['LAB_KIOSK'] ?? '') === '1';
    }

    public static function isOpenPath(string $path): bool
    {
        return $path === '/activate';
    }

    public static function normalize(string $key): string
    {
        $key = strtoupper(trim($key));
        $key = preg_replace('/[^A-Z0-9]/', '', $key) ?? '';
        if (str_starts_with($key, 'PLAB')) {
            $key = substr($key, 4);
        }
        if (strlen($key) !== 16) {
            return '';
        }

        return 'PLAB-' . substr($key, 0, 4) . '-' . substr($key, 4, 4) . '-' . substr($key, 8, 4) . '-' . substr($key, 12, 4);
    }

    public function secret(): string
    {
        $secret = (string) ($this->config['license']['secret'] ?? '');

        return $secret !== '' ? $secret : 'panorama-lab-license-change-me';
    }

    public function serverUrl(): string
    {
        $server = (string) ($this->config['license']['server'] ?? '');
        if ($server === '') {
            $server = (string) ($this->config['app_url'] ?? 'https://magicphotobooth.gr/lab');
        }

        return rtrim($server, '/');
    }

    public function localFile(): string
    {
        foreach ($this->licensePaths() as $path) {
            if ($this->pathWritable($path)) {
                return $path;
            }
        }

        return $this->licensePaths()[0] ?? '';
    }

    /** @return list<string> */
    private function licensePaths(): array
    {
        $out = [];
        $add = static function (string $path) use (&$out): void {
            $path = trim($path);
            if ($path !== '' && ! in_array($path, $out, true)) {
                $out[] = $path;
            }
        };
        $file = $this->env('LAB_LICENSE_FILE');
        if ($file !== '') {
            $add($file);
        }
        $data = $this->env('LAB_DATA_DIR');
        if ($data !== '') {
            $add(rtrim($data, '/\\') . DIRECTORY_SEPARATOR . 'license.json');
        }
        $add((string) ($this->config['license']['file'] ?? ''));
        $home = $this->env('HOME');
        if (PHP_OS_FAMILY === 'Darwin' && $home !== '') {
            $add(rtrim($home, '/') . '/Library/Application Support/Panorama Lab/license.json');
            $add(rtrim($home, '/') . '/Library/Application Support/Panorama Lab/data/license.json');
        }

        return $out;
    }

    private function pathWritable(string $path): bool
    {
        if ($path === '') {
            return false;
        }
        if (is_file($path)) {
            return is_writable($path);
        }
        $dir = dirname($path);
        if (is_dir($dir)) {
            return is_writable($dir);
        }

            return @mkdir($dir, 0755, true) === true && is_writable($dir);
    }

    public function deviceId(): string
    {
        return $this->env('LAB_DEVICE_ID');
    }

    public function deviceName(): string
    {
        $name = $this->env('LAB_DEVICE_NAME');

        return $name !== '' ? $name : (gethostname() ?: 'PC');
    }

    private function env(string $key): string
    {
        $v = getenv($key);
        if (is_string($v) && $v !== '') {
            return trim($v);
        }
        foreach ([$_SERVER[$key] ?? null, $_ENV[$key] ?? null] as $alt) {
            if (is_string($alt) && trim($alt) !== '') {
                return trim($alt);
            }
        }

        return '';
    }

    public function validChecksum(string $key): bool
    {
        $key = self::normalize($key);
        if ($key === '') {
            return false;
        }
        $compact = str_replace('-', '', substr($key, 5));
        $body = substr($compact, 0, 12);
        $mac = substr($compact, 12, 4);

        return hash_equals($this->checksum($body), $mac);
    }

    public function generateKey(): string
    {
        $chars = '';
        $max = strlen(self::ALPHABET) - 1;
        for ($i = 0; $i < 12; $i++) {
            $chars .= self::ALPHABET[random_int(0, $max)];
        }

        return 'PLAB-' . substr($chars, 0, 4) . '-' . substr($chars, 4, 4) . '-' . substr($chars, 8, 4) . '-' . $this->checksum($chars);
    }

    /** @return array<int, array<string, mixed>> */
    public function all(): array
    {
        return $this->pdo->query('SELECT * FROM licenses ORDER BY id DESC')->fetchAll() ?: [];
    }

    /** @return list<string> */
    public function issue(string $note, int $count = 1): array
    {
        $count = max(1, min(20, $count));
        $insert = $this->pdo->prepare('INSERT INTO licenses (code, note) VALUES (:code, :note)');
        $keys = [];
        for ($i = 0; $i < $count; $i++) {
            $code = $this->generateKey();
            $insert->execute(['code' => $code, 'note' => $note]);
            $keys[] = $code;
        }

        return $keys;
    }

    /** @return array{ok: bool, token?: string, key?: string, error?: string, code?: string} */
    public function activate(string $key, string $deviceId, string $deviceName): array
    {
        $key = self::normalize($key);
        $deviceId = trim($deviceId);
        $deviceName = trim($deviceName);
        if ($key === '' || ! $this->validChecksum($key)) {
            return ['ok' => false, 'error' => 'Το κλειδί δεν είναι έγκυρο.', 'code' => 'invalid'];
        }
        if ($deviceId === '' || strlen($deviceId) < 16) {
            return ['ok' => false, 'error' => 'Δεν αναγνωρίστηκε ο υπολογιστής.', 'code' => 'device'];
        }
        $row = $this->find($key);
        if (! $row) {
            return ['ok' => false, 'error' => 'Το κλειδί δεν υπάρχει. Ζήτα άδεια από την Πανόραμα.', 'code' => 'unknown'];
        }
        if ((int) $row['revoked'] === 1) {
            return ['ok' => false, 'error' => 'Το κλειδί έχει απενεργοποιηθεί.', 'code' => 'revoked'];
        }
        $bound = trim((string) ($row['device_id'] ?? ''));
        if ($bound !== '' && ! hash_equals($bound, $deviceId)) {
            return [
                'ok' => false,
                'error' => 'Αυτό το κλειδί χρησιμοποιείται ήδη σε άλλον υπολογιστή. Για δεύτερο PC χρειάζεται νέα άδεια.',
                'code' => 'used',
            ];
        }
        $token = $this->tokenFor($key, $deviceId);
        $now = gmdate('c');
        $this->pdo->prepare(
            'UPDATE licenses
             SET device_id = :device_id,
                 device_name = :device_name,
                 token = :token,
                 activated_at = COALESCE(activated_at, :activated_at),
                 last_seen = :last_seen
             WHERE id = :id'
        )->execute([
            'device_id' => $deviceId,
            'device_name' => $deviceName !== '' ? $deviceName : 'PC',
            'token' => $token,
            'activated_at' => $now,
            'last_seen' => $now,
            'id' => $row['id'],
        ]);

        return ['ok' => true, 'token' => $token, 'key' => $key];
    }

    /** @return array{ok: bool, token?: string, key?: string, error?: string, code?: string} */
    public function check(string $key, string $deviceId, string $token, string $deviceName = ''): array
    {
        $key = self::normalize($key);
        $deviceId = trim($deviceId);
        $token = trim($token);
        if ($key === '' || $deviceId === '' || $token === '') {
            return ['ok' => false, 'error' => 'Λείπει άδεια.', 'code' => 'missing'];
        }
        $row = $this->find($key);
        if (! $row) {
            return ['ok' => false, 'error' => 'Το κλειδί δεν υπάρχει.', 'code' => 'unknown'];
        }
        if ((int) $row['revoked'] === 1) {
            return ['ok' => false, 'error' => 'Το κλειδί έχει απενεργοποιηθεί.', 'code' => 'revoked'];
        }
        $bound = trim((string) ($row['device_id'] ?? ''));
        $saved = trim((string) ($row['token'] ?? ''));
        if ($bound === '' || $saved === '') {
            return ['ok' => false, 'error' => 'Το κλειδί δεν έχει ενεργοποιηθεί ακόμα.', 'code' => 'inactive'];
        }
        $sameDevice = hash_equals($bound, $deviceId);
        $sameToken = hash_equals($saved, $token);
        if (! $sameDevice && ! $sameToken) {
            return [
                'ok' => false,
                'error' => 'Αυτό το κλειδί ανήκει σε άλλον υπολογιστή.',
                'code' => 'used',
            ];
        }
        $tokenOut = $sameDevice ? $saved : $this->tokenFor($key, $deviceId);
        $this->pdo->prepare(
            'UPDATE licenses
             SET device_id = :device_id,
                 token = :token,
                 last_seen = :seen,
                 device_name = COALESCE(NULLIF(:name, ""), device_name)
             WHERE id = :id'
        )->execute([
            'device_id' => $deviceId,
            'token' => $tokenOut,
            'seen' => gmdate('c'),
            'name' => $deviceName,
            'id' => $row['id'],
        ]);

        return ['ok' => true, 'token' => $tokenOut, 'key' => $key];
    }

    public function reset(int $id): void
    {
        $this->pdo->prepare(
            'UPDATE licenses SET device_id = NULL, device_name = NULL, token = NULL, activated_at = NULL, last_seen = NULL WHERE id = :id'
        )->execute(['id' => $id]);
    }

    public function revoke(int $id, bool $revoked = true): void
    {
        $this->pdo->prepare('UPDATE licenses SET revoked = :revoked WHERE id = :id')
            ->execute(['revoked' => $revoked ? 1 : 0, 'id' => $id]);
    }

    public function localIsActive(): bool
    {
        $data = $this->readLocal();

        return $data !== null
            && ($data['key'] ?? '') !== ''
            && ($data['token'] ?? '') !== '';
    }

    /** @param array{ok?: bool, token?: string, key?: string} $result */
    public function writeLocal(array $result, string $deviceId): void
    {
        if (empty($result['ok'])) {
            return;
        }
        $payload = json_encode([
            'key' => (string) ($result['key'] ?? ''),
            'device_id' => $deviceId,
            'token' => (string) ($result['token'] ?? ''),
            'last_ok' => gmdate('c'),
        ], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n";
        $lastError = '';
        foreach ($this->licensePaths() as $file) {
            $dir = dirname($file);
            if (! is_dir($dir) && ! @mkdir($dir, 0755, true) && ! is_dir($dir)) {
                $lastError = 'Δεν δημιουργήθηκε ο φάκελος άδειας.';
                continue;
            }
            if (@file_put_contents($file, $payload) !== false) {
                return;
            }
            $lastError = 'Δεν αποθηκεύτηκε το αρχείο άδειας.';
        }

        throw new \RuntimeException($lastError !== '' ? $lastError : 'Δεν αποθηκεύτηκε η άδεια σε αυτόν τον υπολογιστή.');
    }

    /** @return array<string, mixed>|null */
    public function readLocal(): ?array
    {
        foreach ($this->licensePaths() as $file) {
            if ($file === '' || ! is_file($file)) {
                continue;
            }
            $data = json_decode((string) @file_get_contents($file), true);
            if (is_array($data)) {
                return $data;
            }
        }

        return null;
    }

    /**
     * @param array<string, string> $payload
     * @return array{ok: bool, token?: string, key?: string, error?: string, code?: string}
     */
    public function remote(string $path, array $payload): array
    {
        $url = $this->serverUrl() . $path;
        $body = json_encode($payload, JSON_UNESCAPED_UNICODE);
        $ctx = stream_context_create([
            'http' => [
                'method' => 'POST',
                'header' => "Content-Type: application/json\r\nAccept: application/json\r\n",
                'content' => $body ?: '{}',
                'timeout' => 12,
                'ignore_errors' => true,
            ],
        ]);
        $raw = @file_get_contents($url, false, $ctx);
        if (! is_string($raw) || $raw === '') {
            return ['ok' => false, 'error' => 'Δεν υπάρχει σύνδεση με τον διακομιστή άδειας. Χρειάζεται internet.', 'code' => 'offline'];
        }
        $data = json_decode($raw, true);
        if (! is_array($data)) {
            return ['ok' => false, 'error' => 'Άγνωστη απάντηση διακομιστή.', 'code' => 'bad'];
        }

        return $data;
    }

    public function graceDays(): int
    {
        return (int) ($this->config['license']['grace_days'] ?? self::GRACE_DAYS);
    }

    /** @return array<string, mixed>|null */
    private function find(string $key): ?array
    {
        $stmt = $this->pdo->prepare('SELECT * FROM licenses WHERE code = :code LIMIT 1');
        $stmt->execute(['code' => $key]);
        $row = $stmt->fetch();

        return is_array($row) ? $row : null;
    }

    private function checksum(string $body): string
    {
        $hex = strtoupper(hash_hmac('sha256', $body, $this->secret()));
        $out = '';
        $max = strlen(self::ALPHABET);
        for ($i = 0; $i < 4; $i++) {
            $n = hexdec(substr($hex, $i * 2, 2));
            $out .= self::ALPHABET[$n % $max];
        }

        return $out;
    }

    private function tokenFor(string $key, string $deviceId): string
    {
        return hash_hmac('sha256', $key . '|' . $deviceId, $this->secret());
    }
}
