using System.Globalization;
using Microsoft.Data.Sqlite;
using YMhut.Box.Core.App;

namespace YMhut.Box.Core.Startup;

public interface IStartupCheckStore
{
    string DatabasePath { get; }

    Task SaveAsync(StartupCheckReport report, CancellationToken cancellationToken = default);

    Task<StartupCheckReport?> GetLatestReportAsync(CancellationToken cancellationToken = default);

    Task<StartupCheckReport?> GetLatestReportAsync(StartupCheckKind kind, CancellationToken cancellationToken = default);

    Task<StartupCheckReport?> GetReportAsync(Guid id, CancellationToken cancellationToken = default);

    Task<IReadOnlyList<StartupCheckReportSummary>> GetHistorySummariesAsync(int take = 30, CancellationToken cancellationToken = default);

    Task<IReadOnlyList<StartupCheckReport>> GetHistoryAsync(int take = 30, CancellationToken cancellationToken = default);
}

public sealed class StartupCheckStore : IStartupCheckStore
{
    private readonly SemaphoreSlim _gate = new(1, 1);
    private readonly string _legacyDatabasePath;
    private bool _initialized;

    public StartupCheckStore(AppPaths paths)
    {
        paths.EnsureCreated();
        DatabasePath = AppDatabasePaths.ResolveMainDatabasePath(paths);
        _legacyDatabasePath = Path.Combine(paths.Data, "startup-checks.db");
    }

    public string DatabasePath { get; }

    public async Task SaveAsync(StartupCheckReport report, CancellationToken cancellationToken = default)
    {
        await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            await EnsureInitializedAsync(cancellationToken).ConfigureAwait(false);
            await using var connection = OpenConnection();
            await using var transaction = connection.BeginTransaction();

            await using (var command = connection.CreateCommand())
            {
                command.Transaction = transaction;
                command.CommandText = """
                    INSERT INTO startup_check_reports(
                        id,
                        kind,
                        started_at,
                        completed_at,
                        install_root,
                        manifest_identity,
                        install_identity,
                        is_current_install,
                        superseded_by_current_install,
                        check_basis,
                        issue_count,
                        critical_issue_count,
                        warning_issue_count,
                        visible_issue_count)
                    VALUES (
                        $id,
                        $kind,
                        $started_at,
                        $completed_at,
                        $install_root,
                        $manifest_identity,
                        $install_identity,
                        $is_current_install,
                        $superseded_by_current_install,
                        $check_basis,
                        $issue_count,
                        $critical_issue_count,
                        $warning_issue_count,
                        $visible_issue_count);
                    """;
                command.Parameters.AddWithValue("$id", report.Id.ToString("N"));
                command.Parameters.AddWithValue("$kind", report.Kind.ToString());
                command.Parameters.AddWithValue("$started_at", report.StartedAt.ToString("O", CultureInfo.InvariantCulture));
                command.Parameters.AddWithValue("$completed_at", report.CompletedAt.ToString("O", CultureInfo.InvariantCulture));
                command.Parameters.AddWithValue("$install_root", report.InstallRoot);
                command.Parameters.AddWithValue("$manifest_identity", report.ManifestIdentity);
                command.Parameters.AddWithValue("$install_identity", report.InstallIdentity);
                command.Parameters.AddWithValue("$is_current_install", report.IsCurrentInstall ? 1 : 0);
                command.Parameters.AddWithValue("$superseded_by_current_install", report.SupersededByCurrentInstall ? 1 : 0);
                command.Parameters.AddWithValue("$check_basis", report.CheckBasis);
                command.Parameters.AddWithValue("$issue_count", report.IssueCount);
                command.Parameters.AddWithValue("$critical_issue_count", report.CriticalIssueCount);
                command.Parameters.AddWithValue("$warning_issue_count", report.WarningIssueCount);
                command.Parameters.AddWithValue("$visible_issue_count", report.VisibleIssueCount);
                await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
            }

            foreach (var item in report.Items)
            {
                await using var command = connection.CreateCommand();
                command.Transaction = transaction;
                command.CommandText = """
                    INSERT INTO startup_check_items(
                        report_id,
                        item_id,
                        name,
                        severity,
                        status,
                        detail,
                        path)
                    VALUES (
                        $report_id,
                        $item_id,
                        $name,
                        $severity,
                        $status,
                        $detail,
                        $path);
                    """;
                command.Parameters.AddWithValue("$report_id", report.Id.ToString("N"));
                command.Parameters.AddWithValue("$item_id", item.Id);
                command.Parameters.AddWithValue("$name", item.Name);
                command.Parameters.AddWithValue("$severity", item.Severity.ToString());
                command.Parameters.AddWithValue("$status", item.Status.ToString());
                command.Parameters.AddWithValue("$detail", item.Detail);
                command.Parameters.AddWithValue("$path", (object?)item.Path ?? DBNull.Value);
                await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
            }

            await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
        }
        finally
        {
            _gate.Release();
        }
    }

    public Task<StartupCheckReport?> GetLatestReportAsync(CancellationToken cancellationToken = default)
        => GetLatestCoreAsync(kind: null, cancellationToken);

    public Task<StartupCheckReport?> GetLatestReportAsync(StartupCheckKind kind, CancellationToken cancellationToken = default)
        => GetLatestCoreAsync(kind, cancellationToken);

    public async Task<StartupCheckReport?> GetReportAsync(Guid id, CancellationToken cancellationToken = default)
    {
        await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            await EnsureInitializedAsync(cancellationToken).ConfigureAwait(false);
            await using var connection = OpenConnection();
            await using var command = connection.CreateCommand();
            command.CommandText = """
                SELECT id, kind, started_at, completed_at, install_root, manifest_identity, install_identity, is_current_install, superseded_by_current_install, check_basis, issue_count, critical_issue_count, warning_issue_count, visible_issue_count
                FROM startup_check_reports
                WHERE id = $id
                LIMIT 1;
                """;
            command.Parameters.AddWithValue("$id", id.ToString("N"));

            await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
            if (!await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
            {
                return null;
            }

            var header = ReadHeader(reader);
            return await ReadReportAsync(connection, header, cancellationToken).ConfigureAwait(false);
        }
        finally
        {
            _gate.Release();
        }
    }

    public async Task<IReadOnlyList<StartupCheckReportSummary>> GetHistorySummariesAsync(int take = 30, CancellationToken cancellationToken = default)
    {
        await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            await EnsureInitializedAsync(cancellationToken).ConfigureAwait(false);
            await using var connection = OpenConnection();
            await using var command = connection.CreateCommand();
            command.CommandText = """
                SELECT reports.id,
                       reports.kind,
                       reports.started_at,
                       reports.completed_at,
                       reports.install_root,
                       reports.manifest_identity,
                       reports.install_identity,
                       reports.is_current_install,
                       reports.superseded_by_current_install,
                       reports.check_basis,
                       COUNT(items.id) AS item_count,
                       reports.issue_count,
                       reports.critical_issue_count,
                       reports.warning_issue_count,
                       reports.visible_issue_count
                FROM startup_check_reports reports
                LEFT JOIN startup_check_items items ON items.report_id = reports.id
                GROUP BY reports.id
                ORDER BY reports.completed_at DESC
                LIMIT $take;
                """;
            command.Parameters.AddWithValue("$take", Math.Clamp(take, 1, 200));

            var summaries = new List<StartupCheckReportSummary>();
            await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
            while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
            {
                summaries.Add(ReadSummary(reader));
            }

            return summaries;
        }
        finally
        {
            _gate.Release();
        }
    }

    public async Task<IReadOnlyList<StartupCheckReport>> GetHistoryAsync(int take = 30, CancellationToken cancellationToken = default)
    {
        await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            await EnsureInitializedAsync(cancellationToken).ConfigureAwait(false);
            await using var connection = OpenConnection();
            await using var command = connection.CreateCommand();
            command.CommandText = """
                SELECT id, kind, started_at, completed_at, install_root, manifest_identity, install_identity, is_current_install, superseded_by_current_install, check_basis, issue_count, critical_issue_count, warning_issue_count, visible_issue_count
                FROM startup_check_reports
                ORDER BY completed_at DESC
                LIMIT $take;
                """;
            command.Parameters.AddWithValue("$take", Math.Clamp(take, 1, 200));

            var headers = new List<ReportHeader>();
            await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
            while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
            {
                headers.Add(ReadHeader(reader));
            }

            var reports = new List<StartupCheckReport>(headers.Count);
            foreach (var header in headers)
            {
                reports.Add(await ReadReportAsync(connection, header, cancellationToken).ConfigureAwait(false));
            }

            return reports;
        }
        finally
        {
            _gate.Release();
        }
    }

    private async Task<StartupCheckReport?> GetLatestCoreAsync(StartupCheckKind? kind, CancellationToken cancellationToken)
    {
        await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            await EnsureInitializedAsync(cancellationToken).ConfigureAwait(false);
            await using var connection = OpenConnection();
            await using var command = connection.CreateCommand();
            command.CommandText = kind is null
                ? """
                  SELECT id, kind, started_at, completed_at, install_root, manifest_identity, install_identity, is_current_install, superseded_by_current_install, check_basis, issue_count, critical_issue_count, warning_issue_count, visible_issue_count
                  FROM startup_check_reports
                  ORDER BY completed_at DESC
                  LIMIT 1;
                  """
                : """
                  SELECT id, kind, started_at, completed_at, install_root, manifest_identity, install_identity, is_current_install, superseded_by_current_install, check_basis, issue_count, critical_issue_count, warning_issue_count, visible_issue_count
                  FROM startup_check_reports
                  WHERE kind = $kind
                  ORDER BY completed_at DESC
                  LIMIT 1;
                  """;
            if (kind is not null)
            {
                command.Parameters.AddWithValue("$kind", kind.Value.ToString());
            }

            await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
            if (!await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
            {
                return null;
            }

            var header = ReadHeader(reader);
            return await ReadReportAsync(connection, header, cancellationToken).ConfigureAwait(false);
        }
        finally
        {
            _gate.Release();
        }
    }

    private static async Task<StartupCheckReport> ReadReportAsync(SqliteConnection connection, ReportHeader header, CancellationToken cancellationToken)
    {
        await using var command = connection.CreateCommand();
        command.CommandText = """
            SELECT item_id, name, severity, status, detail, path
            FROM startup_check_items
            WHERE report_id = $report_id
            ORDER BY id;
            """;
        command.Parameters.AddWithValue("$report_id", header.Id.ToString("N"));

        var items = new List<StartupCheckItem>();
        await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
        while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
        {
            items.Add(new StartupCheckItem(
                reader.GetString(0),
                reader.GetString(1),
                ParseEnum<StartupCheckSeverity>(reader.GetString(2), StartupCheckSeverity.Info),
                ParseEnum<StartupCheckStatus>(reader.GetString(3), StartupCheckStatus.Skipped),
                reader.GetString(4),
                reader.IsDBNull(5) ? null : reader.GetString(5)));
        }

        return new StartupCheckReport(
            header.Id,
            header.Kind,
            header.StartedAt,
            header.CompletedAt,
            header.InstallRoot,
            header.ManifestIdentity,
            items,
            header.InstallIdentity,
            header.IsCurrentInstall,
            header.SupersededByCurrentInstall,
            header.CheckBasis);
    }

    private static ReportHeader ReadHeader(SqliteDataReader reader)
    {
        return new ReportHeader(
            Guid.TryParseExact(reader.GetString(0), "N", out var id) ? id : Guid.NewGuid(),
            ParseEnum<StartupCheckKind>(reader.GetString(1), StartupCheckKind.FastPreflight),
            ParseDate(reader.GetString(2)),
            ParseDate(reader.GetString(3)),
            reader.GetString(4),
            reader.GetString(5),
            reader.IsDBNull(6) ? string.Empty : reader.GetString(6),
            ReadBoolean(reader, 7),
            ReadBoolean(reader, 8),
            reader.IsDBNull(9) ? string.Empty : reader.GetString(9));
    }

    private static StartupCheckReportSummary ReadSummary(SqliteDataReader reader)
    {
        return new StartupCheckReportSummary(
            Guid.TryParseExact(reader.GetString(0), "N", out var id) ? id : Guid.NewGuid(),
            ParseEnum<StartupCheckKind>(reader.GetString(1), StartupCheckKind.FastPreflight),
            ParseDate(reader.GetString(2)),
            ParseDate(reader.GetString(3)),
            reader.GetString(4),
            reader.GetString(5),
            ReadInt32(reader, 10),
            ReadInt32(reader, 11),
            ReadInt32(reader, 12),
            ReadInt32(reader, 13),
            ReadInt32(reader, 14),
            reader.IsDBNull(6) ? string.Empty : reader.GetString(6),
            ReadBoolean(reader, 7),
            ReadBoolean(reader, 8),
            reader.IsDBNull(9) ? string.Empty : reader.GetString(9));
    }

    private static int ReadInt32(SqliteDataReader reader, int ordinal)
        => Convert.ToInt32(reader.GetValue(ordinal), CultureInfo.InvariantCulture);

    private static bool ReadBoolean(SqliteDataReader reader, int ordinal)
        => !reader.IsDBNull(ordinal) && Convert.ToInt32(reader.GetValue(ordinal), CultureInfo.InvariantCulture) != 0;

    private async Task EnsureInitializedAsync(CancellationToken cancellationToken)
    {
        if (_initialized)
        {
            return;
        }

        Directory.CreateDirectory(Path.GetDirectoryName(DatabasePath)!);
        await using var connection = OpenConnection();
        await using var command = connection.CreateCommand();
        command.CommandText = """
            PRAGMA journal_mode = WAL;
            PRAGMA synchronous = NORMAL;
            PRAGMA busy_timeout = 5000;
            CREATE TABLE IF NOT EXISTS startup_check_reports (
                id TEXT PRIMARY KEY,
                kind TEXT NOT NULL,
                started_at TEXT NOT NULL,
                completed_at TEXT NOT NULL,
                install_root TEXT NOT NULL,
                manifest_identity TEXT NOT NULL,
                install_identity TEXT NOT NULL DEFAULT '',
                is_current_install INTEGER NOT NULL DEFAULT 1,
                superseded_by_current_install INTEGER NOT NULL DEFAULT 0,
                check_basis TEXT NOT NULL DEFAULT '',
                issue_count INTEGER NOT NULL,
                critical_issue_count INTEGER NOT NULL,
                warning_issue_count INTEGER NOT NULL,
                visible_issue_count INTEGER NOT NULL DEFAULT 0
            );
            CREATE TABLE IF NOT EXISTS startup_check_items (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                report_id TEXT NOT NULL,
                item_id TEXT NOT NULL,
                name TEXT NOT NULL,
                severity TEXT NOT NULL,
                status TEXT NOT NULL,
                detail TEXT NOT NULL,
                path TEXT NULL,
                FOREIGN KEY(report_id) REFERENCES startup_check_reports(id) ON DELETE CASCADE
            );
            CREATE INDEX IF NOT EXISTS idx_startup_check_reports_completed ON startup_check_reports(completed_at DESC);
            CREATE INDEX IF NOT EXISTS idx_startup_check_reports_kind_completed ON startup_check_reports(kind, completed_at DESC);
            CREATE INDEX IF NOT EXISTS idx_startup_check_items_report ON startup_check_items(report_id);
            """;
        await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
        await EnsureColumnAsync(connection, "startup_check_reports", "install_identity", "TEXT NOT NULL DEFAULT ''", cancellationToken).ConfigureAwait(false);
        await EnsureColumnAsync(connection, "startup_check_reports", "is_current_install", "INTEGER NOT NULL DEFAULT 1", cancellationToken).ConfigureAwait(false);
        await EnsureColumnAsync(connection, "startup_check_reports", "superseded_by_current_install", "INTEGER NOT NULL DEFAULT 0", cancellationToken).ConfigureAwait(false);
        await EnsureColumnAsync(connection, "startup_check_reports", "check_basis", "TEXT NOT NULL DEFAULT ''", cancellationToken).ConfigureAwait(false);
        await EnsureColumnAsync(connection, "startup_check_reports", "visible_issue_count", "INTEGER NOT NULL DEFAULT 0", cancellationToken).ConfigureAwait(false);
        await ImportLegacyDatabaseAsync(connection, cancellationToken).ConfigureAwait(false);
        _initialized = true;
    }

    private async Task ImportLegacyDatabaseAsync(SqliteConnection connection, CancellationToken cancellationToken)
    {
        if (!File.Exists(_legacyDatabasePath) ||
            string.Equals(Path.GetFullPath(_legacyDatabasePath), Path.GetFullPath(DatabasePath), StringComparison.OrdinalIgnoreCase))
        {
            return;
        }

        try
        {
            await using (var attach = connection.CreateCommand())
            {
                attach.CommandText = "ATTACH DATABASE $path AS legacy_startup;";
                attach.Parameters.AddWithValue("$path", _legacyDatabasePath);
                await attach.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
            }

            var legacyHasVisibleIssueCount = await LegacyTableHasColumnAsync(
                connection,
                "startup_check_reports",
                "visible_issue_count",
                cancellationToken).ConfigureAwait(false);

            await using (var command = connection.CreateCommand())
            {
                var visibleIssueExpression = legacyHasVisibleIssueCount ? "visible_issue_count" : "issue_count";
                command.CommandText = $$"""
                    INSERT OR IGNORE INTO startup_check_reports(
                        id,
                        kind,
                        started_at,
                        completed_at,
                        install_root,
                        manifest_identity,
                        install_identity,
                        is_current_install,
                        superseded_by_current_install,
                        check_basis,
                        issue_count,
                        critical_issue_count,
                        warning_issue_count,
                        visible_issue_count)
                    SELECT id,
                           kind,
                           started_at,
                           completed_at,
                           install_root,
                           manifest_identity,
                           '',
                           0,
                           1,
                           'legacy-import',
                           issue_count,
                           critical_issue_count,
                           warning_issue_count,
                           {{visibleIssueExpression}}
                    FROM legacy_startup.startup_check_reports;

                    INSERT OR IGNORE INTO startup_check_items(
                        report_id,
                        item_id,
                        name,
                        severity,
                        status,
                        detail,
                        path)
                    SELECT report_id,
                           item_id,
                           name,
                           severity,
                           status,
                           detail,
                           path
                    FROM legacy_startup.startup_check_items;
                    """;
                await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
            }

            await using (var detach = connection.CreateCommand())
            {
                detach.CommandText = "DETACH DATABASE legacy_startup;";
                await detach.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
            }

            DeleteLegacyDatabaseFiles();
        }
        catch
        {
            try
            {
                await using var detach = connection.CreateCommand();
                detach.CommandText = "DETACH DATABASE legacy_startup;";
                await detach.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
            }
            catch
            {
            }
        }
    }

    private static async Task<bool> LegacyTableHasColumnAsync(
        SqliteConnection connection,
        string table,
        string column,
        CancellationToken cancellationToken)
    {
        await using var command = connection.CreateCommand();
        command.CommandText = $"PRAGMA legacy_startup.table_info({table});";
        await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
        while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
        {
            if (string.Equals(reader.GetString(1), column, StringComparison.OrdinalIgnoreCase))
            {
                return true;
            }
        }

        return false;
    }

    private void DeleteLegacyDatabaseFiles()
    {
        foreach (var path in new[]
        {
            _legacyDatabasePath,
            _legacyDatabasePath + "-wal",
            _legacyDatabasePath + "-shm"
        })
        {
            try
            {
                if (File.Exists(path))
                {
                    File.Delete(path);
                }
            }
            catch
            {
            }
        }
    }

    private static async Task EnsureColumnAsync(
        SqliteConnection connection,
        string table,
        string column,
        string definition,
        CancellationToken cancellationToken)
    {
        await using (var command = connection.CreateCommand())
        {
            command.CommandText = $"PRAGMA table_info({table});";
            await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
            while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
            {
                if (string.Equals(reader.GetString(1), column, StringComparison.OrdinalIgnoreCase))
                {
                    return;
                }
            }
        }

        await using (var command = connection.CreateCommand())
        {
            command.CommandText = $"ALTER TABLE {table} ADD COLUMN {column} {definition};";
            await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
        }
    }

    private SqliteConnection OpenConnection()
    {
        var connection = new SqliteConnection($"Data Source={DatabasePath};Pooling=False");
        connection.Open();
        using var command = connection.CreateCommand();
        command.CommandText = """
            PRAGMA busy_timeout = 5000;
            PRAGMA synchronous = NORMAL;
            """;
        command.ExecuteNonQuery();
        return connection;
    }

    private static DateTimeOffset ParseDate(string value)
        => DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out var parsed)
            ? parsed
            : DateTimeOffset.Now;

    private static TEnum ParseEnum<TEnum>(string value, TEnum fallback)
        where TEnum : struct
        => Enum.TryParse<TEnum>(value, ignoreCase: true, out var parsed) ? parsed : fallback;

    private sealed record ReportHeader(
        Guid Id,
        StartupCheckKind Kind,
        DateTimeOffset StartedAt,
        DateTimeOffset CompletedAt,
        string InstallRoot,
        string ManifestIdentity,
        string InstallIdentity,
        bool IsCurrentInstall,
        bool SupersededByCurrentInstall,
        string CheckBasis);
}
