<?php
/**
 * Antara — encrypted JSON store (AES-256-GCM) for users, WebAuthn credentials,
 * auth_config and a transactional helper API.
 *
 * Transactional pattern:
 *   AntaraStore::transact('users', function (&$blob) use ($payload) {
 *       $blob['users'][] = $payload;
 *   });
 *
 * The "lockfile" is a sentinel `<name>.lock` next to the encrypted file. We
 * acquire LOCK_EX on it for the read-modify-write window. The encrypted file
 * itself is then replaced atomically via AntaraCrypto::writeAtomic().
 */

require_once __DIR__ . '/../security/crypto.php';
require_once __DIR__ . '/../security/paths.php';

final class AntaraStore
{
    private const KIND_USERS  = 'users';
    private const KIND_CREDS  = 'creds';
    private const KIND_RATE   = 'rate';
    private const KIND_CONFIG = 'config';

    public const ROLE_SUPER = 'super_admin';
    public const ROLE_ADMIN = 'admin';
    public const ROLE_USER  = 'usuario';

    public const ROLES = [self::ROLE_SUPER, self::ROLE_ADMIN, self::ROLE_USER];

    /** Default app config; written on first call to configRead() if missing. */
    public static function defaultConfig(): array
    {
        return [
            'version'                 => 1,
            'login_required'          => false,
            'allow_user_registration' => true,
            'allow_webauthn'          => true,
            'allow_password_login'    => true,
            /** When true and there are zero users, the desktop loads without forcing first-user registration. */
            'public_desktop_without_users' => false,
            'session_timeout_minutes' => 60,
            'max_failed_attempts'     => 5,
            'lockout_minutes'         => 15,
            'rp_name'                 => 'FLEXIA',
            'rp_id'                   => null,
            'updated_at'              => gmdate('Y-m-d\TH:i:s\Z'),
        ];
    }

    public static function defaultUsersBlob(): array
    {
        return ['version' => 1, 'users' => []];
    }

    public static function defaultCredsBlob(): array
    {
        return ['version' => 1, 'credentials' => []];
    }

    public static function defaultRateBlob(): array
    {
        return ['version' => 1, 'entries' => []];
    }

    private static function fileFor(string $kind): string
    {
        switch ($kind) {
            case self::KIND_USERS:  return AntaraPaths::usersFile();
            case self::KIND_CREDS:  return AntaraPaths::credsFile();
            case self::KIND_RATE:   return AntaraPaths::rateFile();
            case self::KIND_CONFIG: return AntaraPaths::configFile();
        }
        throw new InvalidArgumentException('Unknown store kind: ' . $kind);
    }

    private static function defaultFor(string $kind): array
    {
        switch ($kind) {
            case self::KIND_USERS:  return self::defaultUsersBlob();
            case self::KIND_CREDS:  return self::defaultCredsBlob();
            case self::KIND_RATE:   return self::defaultRateBlob();
            case self::KIND_CONFIG: return self::defaultConfig();
        }
        throw new InvalidArgumentException('Unknown store kind: ' . $kind);
    }

    private static function read(string $kind): array
    {
        $file = self::fileFor($kind);
        if (!is_file($file)) {
            $blob = self::defaultFor($kind);
            self::write($kind, $blob);
            return $blob;
        }
        $payload = @file_get_contents($file);
        if ($payload === false || $payload === '') {
            return self::defaultFor($kind);
        }
        try {
            $data = AntaraCrypto::decryptJson($payload, $kind);
            return is_array($data) ? $data : self::defaultFor($kind);
        } catch (\Throwable $e) {
            // Decrypt failure: do NOT silently overwrite; surface the error.
            throw new RuntimeException('AntaraStore: cannot read ' . $kind . ' (' . $e->getMessage() . ')');
        }
    }

    private static function write(string $kind, array $blob): void
    {
        $file = self::fileFor($kind);
        $payload = AntaraCrypto::encryptJson($blob, $kind);
        if (!AntaraCrypto::writeAtomic($file, $payload)) {
            throw new RuntimeException('AntaraStore: cannot persist ' . $kind);
        }
    }

    /**
     * Acquire an exclusive lock for the kind, run $fn(&$blob), persist if
     * the callback returns true (default). Returns the (possibly mutated) blob.
     */
    public static function transact(string $kind, callable $fn): array
    {
        AntaraPaths::ensureDirs();
        $lockPath = AntaraPaths::lockFile($kind);
        $fp = @fopen($lockPath, 'c');
        if (!$fp) throw new RuntimeException('AntaraStore: cannot open lock for ' . $kind);
        try {
            if (!flock($fp, LOCK_EX)) {
                throw new RuntimeException('AntaraStore: lock denied for ' . $kind);
            }
            $blob = self::read($kind);
            $shouldWrite = $fn($blob);
            if ($shouldWrite === null || $shouldWrite === true) {
                self::write($kind, $blob);
            }
            return $blob;
        } finally {
            @flock($fp, LOCK_UN);
            @fclose($fp);
        }
    }

    /**
     * Vacía el almacén cifrado de usuarios y el de credenciales WebAuthn.
     * Usado al reabrir el instalador o al crear la primera cuenta desde el
     * asistente, para evitar conflictos con cuentas residuales.
     */
    public static function resetUsersAndCredentials(): void
    {
        self::transact(self::KIND_USERS, function (&$blob) {
            $blob = self::defaultUsersBlob();
        });
        self::transact(self::KIND_CREDS, function (&$blob) {
            $blob = self::defaultCredsBlob();
        });
    }

    /* ---------------- Convenience: USERS ---------------- */

    public static function usersAll(): array
    {
        return self::read(self::KIND_USERS);
    }

    public static function findUserById(string $id): ?array
    {
        foreach (self::usersAll()['users'] as $u) {
            if (($u['id'] ?? '') === $id && empty($u['deletedAt'])) return $u;
        }
        return null;
    }

    public static function findUserByLogin(string $login): ?array
    {
        $login = trim($login);
        if ($login === '') return null;

        $loginLower = strtolower($login);
        $loginDigits = AntaraSanitize::phoneDigits($login);
        $matchByPhone = $loginDigits !== '' && strlen($loginDigits) >= 6;

        foreach (self::usersAll()['users'] as $u) {
            if (!empty($u['deletedAt'])) continue;
            if (strtolower($u['email'] ?? '') === $loginLower) return $u;
            if (strtolower($u['username'] ?? '') === $loginLower) return $u;
            if ($matchByPhone) {
                $storedDigits = AntaraSanitize::phoneDigits($u['phone'] ?? '');
                if ($storedDigits !== '' && $storedDigits === $loginDigits) return $u;
            }
        }
        return null;
    }

    /**
     * Hay al menos un usuario activo (no borrado) con rol super_admin.
     */
    public static function hasActiveSuperAdmin(): bool
    {
        try {
            foreach (self::usersAll()['users'] ?? [] as $u) {
                if (($u['role'] ?? '') === self::ROLE_SUPER && empty($u['deletedAt'])) {
                    return true;
                }
            }
        } catch (\Throwable $e) {
            return false;
        }
        return false;
    }

    /**
     * Si no hay ningún super_admin activo, asigna ese rol al usuario dado (reparación
     * de instancias sin super). No hace nada si ya existe un super_admin activo.
     *
     * @return array|null Fila de usuario actualizada al promover; null si no aplica.
     */
    public static function promoteToSuperIfNoSuper(string $userId): ?array
    {
        if (self::hasActiveSuperAdmin()) {
            return null;
        }
        $u = self::findUserById($userId);
        if (!$u) {
            return null;
        }
        if (!($u['active'] ?? true) || !empty($u['blocked'])) {
            return null;
        }
        return self::upsertUser(['role' => self::ROLE_SUPER], $userId);
    }

    public static function upsertUser(array $patch, ?string $id = null): array
    {
        $now = gmdate('Y-m-d\TH:i:s\Z');
        $resultUser = null;
        self::transact(self::KIND_USERS, function (&$blob) use ($patch, $id, $now, &$resultUser) {
            $blob['users'] = $blob['users'] ?? [];
            $found = false;
            foreach ($blob['users'] as $i => $u) {
                if (($u['id'] ?? '') === ($id ?: ($patch['id'] ?? null))) {
                    $blob['users'][$i] = array_merge($u, $patch, ['updatedAt' => $now]);
                    $resultUser = $blob['users'][$i];
                    $found = true;
                    break;
                }
            }
            if (!$found) {
                $newUser = array_merge([
                    'id'         => self::uuid(),
                    'username'   => '',
                    'email'      => '',
                    'phone'      => '',
                    'role'       => self::ROLE_USER,
                    'active'     => true,
                    'blocked'    => false,
                    'deletedAt'  => null,
                    'createdAt'  => $now,
                    'updatedAt'  => $now,
                    'lastLoginAt'=> null,
                    'failedAttempts' => 0,
                    'lockedUntil'=> null,
                    'webauthnCredentialIds' => [],
                    'branchId'   => '',
                    'departmentId' => '',
                    'positionId' => '',
                ], $patch);
                $blob['users'][] = $newUser;
                $resultUser = $newUser;
            }
        });
        return $resultUser;
    }

    public static function softDeleteUser(string $id): void
    {
        $now = gmdate('Y-m-d\TH:i:s\Z');
        self::transact(self::KIND_USERS, function (&$blob) use ($id, $now) {
            foreach ($blob['users'] as $i => $u) {
                if (($u['id'] ?? '') === $id) {
                    $blob['users'][$i]['deletedAt'] = $now;
                    $blob['users'][$i]['active'] = false;
                    return;
                }
            }
        });
    }

    /* ---------------- Convenience: CREDENTIALS ---------------- */

    public static function credsForUser(string $userId): array
    {
        $out = [];
        foreach (self::read(self::KIND_CREDS)['credentials'] ?? [] as $c) {
            if (($c['userId'] ?? '') === $userId) $out[] = $c;
        }
        return $out;
    }

    public static function credByCredentialId(string $credIdB64u): ?array
    {
        foreach (self::read(self::KIND_CREDS)['credentials'] ?? [] as $c) {
            if (($c['credentialId'] ?? '') === $credIdB64u) return $c;
        }
        return null;
    }

    public static function addCredential(array $cred): void
    {
        self::transact(self::KIND_CREDS, function (&$blob) use ($cred) {
            $blob['credentials'][] = $cred;
        });
        // Also push the id reference into the user record.
        if (!empty($cred['userId']) && !empty($cred['credentialId'])) {
            $u = self::findUserById($cred['userId']);
            if ($u) {
                $ids = $u['webauthnCredentialIds'] ?? [];
                if (!in_array($cred['credentialId'], $ids, true)) {
                    $ids[] = $cred['credentialId'];
                    self::upsertUser(['id' => $u['id'], 'webauthnCredentialIds' => $ids]);
                }
            }
        }
    }

    public static function updateCredentialSignCount(string $credIdB64u, int $newCount): void
    {
        $now = gmdate('Y-m-d\TH:i:s\Z');
        self::transact(self::KIND_CREDS, function (&$blob) use ($credIdB64u, $newCount, $now) {
            foreach ($blob['credentials'] as $i => $c) {
                if (($c['credentialId'] ?? '') === $credIdB64u) {
                    $blob['credentials'][$i]['signCount'] = $newCount;
                    $blob['credentials'][$i]['lastUsedAt'] = $now;
                    return;
                }
            }
        });
    }

    public static function deleteCredentialsForUser(string $userId): void
    {
        self::transact(self::KIND_CREDS, function (&$blob) use ($userId) {
            $blob['credentials'] = array_values(array_filter(
                $blob['credentials'] ?? [],
                fn($c) => ($c['userId'] ?? '') !== $userId
            ));
        });
        $u = self::findUserById($userId);
        if ($u) {
            self::upsertUser(['id' => $u['id'], 'webauthnCredentialIds' => []]);
        }
    }

    /* ---------------- Convenience: RATE LIMIT ---------------- */

    public static function rateRead(): array { return self::read(self::KIND_RATE); }

    public static function rateTransact(callable $fn): array
    {
        return self::transact(self::KIND_RATE, $fn);
    }

    /* ---------------- Convenience: CONFIG ---------------- */

    /**
     * @return null When ciphertext is missing decryptable (wrong key / corrupt); callers show recovery UI.
     */
    public static function tryConfigRead(): ?array
    {
        try {
            return self::configRead();
        } catch (\RuntimeException $e) {
            return null;
        }
    }

    public static function configRead(): array
    {
        $cfg = self::read(self::KIND_CONFIG);
        // Merge defaults to tolerate missing keys after upgrades.
        return array_merge(self::defaultConfig(), $cfg);
    }

    public static function configWrite(array $cfg): array
    {
        $merged = null;
        self::transact(self::KIND_CONFIG, function (&$blob) use ($cfg, &$merged) {
            $blob = array_merge(self::defaultConfig(), $blob, $cfg, [
                'updated_at' => gmdate('Y-m-d\TH:i:s\Z'),
            ]);
            $merged = $blob;
        });
        return $merged;
    }

    /* ---------------- Helpers ---------------- */

    public static function uuid(): string
    {
        $b = random_bytes(16);
        $b[6] = chr((ord($b[6]) & 0x0f) | 0x40); // version 4
        $b[8] = chr((ord($b[8]) & 0x3f) | 0x80); // variant
        $h = bin2hex($b);
        return sprintf('%s-%s-%s-%s-%s', substr($h,0,8), substr($h,8,4), substr($h,12,4), substr($h,16,4), substr($h,20,12));
    }
}
