<?php

declare(strict_types=1);

final class MyronUpdateStateRepository
{
    private const ROW_ID = 'default';

    public function __construct(private readonly PDO $pdo)
    {
    }

    /**
     * @return array<string, mixed>
     */
    public function get(): array
    {
        $statement = $this->pdo->prepare('SELECT * FROM application_update_state WHERE id = :id LIMIT 1');
        $statement->execute([':id' => self::ROW_ID]);
        $row = $statement->fetch();

        if (!is_array($row)) {
            return $this->emptyState();
        }

        $availableUpdate = null;
        $availableJson = trim((string) ($row['available_update_json'] ?? ''));
        if ($availableJson !== '') {
            try {
                $decoded = json_decode($availableJson, true, 512, JSON_THROW_ON_ERROR);
                $availableUpdate = is_array($decoded) ? $decoded : null;
            } catch (Throwable) {
                $availableUpdate = null;
            }
        }

        return $this->normalizeForCurrentRelease([
            'installed_version' => (string) ($row['installed_version'] ?? MyronReleaseMetadata::VERSION),
            'last_checked_at' => (string) ($row['last_checked_at'] ?? ''),
            'last_manual_checked_at' => (string) ($row['last_manual_checked_at'] ?? ''),
            'last_check_status' => (string) ($row['last_check_status'] ?? ''),
            'last_check_error' => (string) ($row['last_check_error'] ?? ''),
            'available_update' => $availableUpdate,
            'dismissed_update_version' => (string) ($row['dismissed_update_version'] ?? ''),
            'updated_at' => (string) ($row['updated_at'] ?? ''),
        ]);
    }

    /**
     * @param array<string, mixed>|null $availableUpdate
     */
    public function recordCheck(string $status, ?array $availableUpdate, string $error = '', bool $manual = false): void
    {
        $now = myronNow();
        $state = $this->get();
        $manualCheckedAt = $manual ? $now : (string) ($state['last_manual_checked_at'] ?? '');
        $availableJson = $availableUpdate === null ? '' : json_encode($availableUpdate, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);

        $this->upsert([
            'installed_version' => MyronReleaseMetadata::VERSION,
            'last_checked_at' => $now,
            'last_manual_checked_at' => $manualCheckedAt,
            'last_check_status' => $status,
            'last_check_error' => $error,
            'available_update_json' => $availableJson,
            'dismissed_update_version' => (string) ($state['dismissed_update_version'] ?? ''),
            'updated_at' => $now,
        ]);
    }

    public function dismissVersion(string $version): void
    {
        $state = $this->get();
        $this->upsert([
            'installed_version' => MyronReleaseMetadata::VERSION,
            'last_checked_at' => (string) ($state['last_checked_at'] ?? ''),
            'last_manual_checked_at' => (string) ($state['last_manual_checked_at'] ?? ''),
            'last_check_status' => (string) ($state['last_check_status'] ?? ''),
            'last_check_error' => (string) ($state['last_check_error'] ?? ''),
            'available_update_json' => is_array($state['available_update'] ?? null)
                ? json_encode($state['available_update'], JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES)
                : '',
            'dismissed_update_version' => trim($version),
            'updated_at' => myronNow(),
        ]);
    }

    /**
     * @param array<string, mixed> $applyDetails
     */
    public function recordAppliedUpdate(array $applyDetails = []): void
    {
        $state = $this->get();
        $targetVersion = trim((string) ($applyDetails['targetVersion'] ?? ''));
        $installedVersion = $targetVersion !== '' ? $targetVersion : $this->effectiveInstalledVersion((string) ($state['installed_version'] ?? ''));
        $now = myronNow();

        $this->upsert([
            'installed_version' => $installedVersion,
            'last_checked_at' => (string) ($state['last_checked_at'] ?? ''),
            'last_manual_checked_at' => (string) ($state['last_manual_checked_at'] ?? ''),
            'last_check_status' => 'up_to_date',
            'last_check_error' => '',
            'available_update_json' => '',
            'dismissed_update_version' => (string) ($state['dismissed_update_version'] ?? ''),
            'updated_at' => $now,
        ]);
    }

    public function hasVisibleAvailableUpdate(): bool
    {
        $state = $this->get();
        $available = is_array($state['available_update'] ?? null) ? $state['available_update'] : null;
        if ($available === null) {
            return false;
        }

        $latestVersion = (string) ($available['latestVersion'] ?? '');
        $package = is_array($available['package'] ?? null) ? $available['package'] : [];
        return $latestVersion !== ''
            && trim((string) ($package['url'] ?? '')) !== ''
            && $latestVersion !== (string) ($state['dismissed_update_version'] ?? '');
    }

    /**
     * @return array<string, mixed>
     */
    private function emptyState(): array
    {
        return [
            'installed_version' => MyronReleaseMetadata::VERSION,
            'last_checked_at' => '',
            'last_manual_checked_at' => '',
            'last_check_status' => '',
            'last_check_error' => '',
            'available_update' => null,
            'dismissed_update_version' => '',
            'updated_at' => '',
        ];
    }

    /**
     * @param array<string, mixed> $state
     * @return array<string, mixed>
     */
    private function normalizeForCurrentRelease(array $state): array
    {
        $state['installed_version'] = $this->effectiveInstalledVersion((string) ($state['installed_version'] ?? ''));

        $available = is_array($state['available_update'] ?? null) ? $state['available_update'] : null;
        if ($available === null) {
            return $state;
        }

        $latestVersion = trim((string) ($available['latestVersion'] ?? ''));
        if ($latestVersion === '' || !$this->isVersionNewerThan($latestVersion, (string) $state['installed_version'])) {
            $state['available_update'] = null;
            if ((string) ($state['last_check_status'] ?? '') === 'update_available') {
                $state['last_check_status'] = 'up_to_date';
                $state['last_check_error'] = '';
            }
        }

        return $state;
    }

    private function isVersionNewerThanCurrent(string $version): bool
    {
        return $this->isVersionNewerThan($version, MyronReleaseMetadata::VERSION);
    }

    private function isVersionNewerThan(string $version, string $baselineVersion): bool
    {
        if (class_exists(MyronVersionComparator::class)) {
            try {
                return (new MyronVersionComparator())->isNewer($version, $baselineVersion);
            } catch (Throwable) {
                return false;
            }
        }

        return version_compare(ltrim($version, 'v'), ltrim($baselineVersion, 'v'), '>');
    }

    private function effectiveInstalledVersion(string $persistedVersion): string
    {
        $persistedVersion = trim($persistedVersion);
        if ($persistedVersion === '') {
            return MyronReleaseMetadata::VERSION;
        }

        return $this->isVersionNewerThan($persistedVersion, MyronReleaseMetadata::VERSION)
            ? $persistedVersion
            : MyronReleaseMetadata::VERSION;
    }

    /**
     * @param array<string, mixed> $row
     */
    private function upsert(array $row): void
    {
        $sql = 'INSERT INTO application_update_state (
                id,
                installed_version,
                last_checked_at,
                last_manual_checked_at,
                last_check_status,
                last_check_error,
                available_update_json,
                dismissed_update_version,
                updated_at
            ) VALUES (
                :id,
                :installed_version,
                :last_checked_at,
                :last_manual_checked_at,
                :last_check_status,
                :last_check_error,
                :available_update_json,
                :dismissed_update_version,
                :updated_at
            )
            ON DUPLICATE KEY UPDATE
                installed_version = VALUES(installed_version),
                last_checked_at = VALUES(last_checked_at),
                last_manual_checked_at = VALUES(last_manual_checked_at),
                last_check_status = VALUES(last_check_status),
                last_check_error = VALUES(last_check_error),
                available_update_json = VALUES(available_update_json),
                dismissed_update_version = VALUES(dismissed_update_version),
                updated_at = VALUES(updated_at)';

        if ($this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME) === 'sqlite') {
            $sql = 'INSERT INTO application_update_state (
                    id,
                    installed_version,
                    last_checked_at,
                    last_manual_checked_at,
                    last_check_status,
                    last_check_error,
                    available_update_json,
                    dismissed_update_version,
                    updated_at
                ) VALUES (
                    :id,
                    :installed_version,
                    :last_checked_at,
                    :last_manual_checked_at,
                    :last_check_status,
                    :last_check_error,
                    :available_update_json,
                    :dismissed_update_version,
                    :updated_at
                )
                ON CONFLICT(id) DO UPDATE SET
                    installed_version = excluded.installed_version,
                    last_checked_at = excluded.last_checked_at,
                    last_manual_checked_at = excluded.last_manual_checked_at,
                    last_check_status = excluded.last_check_status,
                    last_check_error = excluded.last_check_error,
                    available_update_json = excluded.available_update_json,
                    dismissed_update_version = excluded.dismissed_update_version,
                    updated_at = excluded.updated_at';
        }

        $statement = $this->pdo->prepare($sql);
        $statement->execute([
            ':id' => self::ROW_ID,
            ':installed_version' => (string) ($row['installed_version'] ?? MyronReleaseMetadata::VERSION),
            ':last_checked_at' => (string) ($row['last_checked_at'] ?? ''),
            ':last_manual_checked_at' => (string) ($row['last_manual_checked_at'] ?? ''),
            ':last_check_status' => (string) ($row['last_check_status'] ?? ''),
            ':last_check_error' => (string) ($row['last_check_error'] ?? ''),
            ':available_update_json' => (string) ($row['available_update_json'] ?? ''),
            ':dismissed_update_version' => (string) ($row['dismissed_update_version'] ?? ''),
            ':updated_at' => (string) ($row['updated_at'] ?? myronNow()),
        ]);
    }
}
