using System.Text;
using System.Text.Json;
using Couchbase.Lite;
using Couchbase.Lite.Query;

namespace WorkTracker.Services.Storage;

public sealed class JsonDatabaseBackupService(CouchbaseLiteDatabaseProvider databaseProvider) : IDatabaseBackupService
{
    private const int CurrentBackupFormatVersion = 1;

    private static readonly JsonSerializerOptions JsonSerializerOptions = new()
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
        PropertyNameCaseInsensitive = true,
        WriteIndented = true
    };

    public Task<DatabaseBackupFile> ExportAsync(CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();
        EnsureSupportedDatabaseSchemaVersion(databaseProvider.DatabaseSchemaVersion);

        var payload = new DatabaseBackupPayload
        {
            BackupFormatVersion = CurrentBackupFormatVersion,
            DatabaseSchemaVersion = databaseProvider.DatabaseSchemaVersion,
            ExportedAtUtc = DateTimeOffset.UtcNow,
            Collections = ExportCollections(cancellationToken)
        };

        var content = JsonSerializer.SerializeToUtf8Bytes(payload, JsonSerializerOptions);
        var timestamp = payload.ExportedAtUtc.ToString("yyyyMMdd-HHmmss");

        return Task.FromResult(new DatabaseBackupFile
        {
            Content = content,
            ContentType = "application/json",
            FileName = $"worktracker-backup-v{payload.DatabaseSchemaVersion}-{timestamp}.json"
        });
    }

    public async Task ImportAsync(Stream jsonStream, CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(jsonStream);
        cancellationToken.ThrowIfCancellationRequested();

        DatabaseBackupPayload payload;

        try
        {
            payload = await JsonSerializer.DeserializeAsync<DatabaseBackupPayload>(jsonStream, JsonSerializerOptions, cancellationToken)
                ?? throw new DatabaseBackupException("Backup file is empty or invalid.");
        }
        catch (JsonException exception)
        {
            throw new DatabaseBackupException($"Backup file is not valid JSON: {exception.Message}");
        }

        ValidatePayload(payload);

        databaseProvider.ExecuteInBatch(() =>
        {
            foreach (var collection in databaseProvider.BackupCollections.Values)
            {
                ClearCollection(collection);
            }

            foreach (var (collectionName, documents) in payload.Collections)
            {
                var collection = databaseProvider.BackupCollections[collectionName];

                foreach (var document in documents)
                {
                    var mutableDocument = new MutableDocument(document.Id);
                    mutableDocument.SetJSON(document.Data.GetRawText());
                    collection.Save(mutableDocument);
                }
            }
        });
    }

    private Dictionary<string, List<DatabaseBackupDocumentPayload>> ExportCollections(CancellationToken cancellationToken)
    {
        var collections = new Dictionary<string, List<DatabaseBackupDocumentPayload>>(StringComparer.Ordinal);

        foreach (var (collectionName, collection) in databaseProvider.BackupCollections)
        {
            cancellationToken.ThrowIfCancellationRequested();

            collections[collectionName] = GetDocuments(collection, cancellationToken)
                .Select(document => new DatabaseBackupDocumentPayload
                {
                    Id = document.Id,
                    Data = ParseJson(document.ToJSON())
                })
                .ToList();
        }

        return collections;
    }

    private void ValidatePayload(DatabaseBackupPayload payload)
    {
        if (payload.BackupFormatVersion <= 0)
        {
            throw new DatabaseBackupException("Backup file does not declare a supported format version.");
        }

        EnsureSupportedBackupFormatVersion(payload.BackupFormatVersion);
        EnsureSupportedDatabaseSchemaVersion(payload.DatabaseSchemaVersion);

        if (payload.DatabaseSchemaVersion != databaseProvider.DatabaseSchemaVersion)
        {
            throw new DatabaseBackupException($"Backup schema version {payload.DatabaseSchemaVersion} does not match database schema version {databaseProvider.DatabaseSchemaVersion}.");
        }

        var expectedCollectionNames = databaseProvider.BackupCollections.Keys.OrderBy(static value => value, StringComparer.Ordinal).ToArray();
        var actualCollectionNames = payload.Collections.Keys.OrderBy(static value => value, StringComparer.Ordinal).ToArray();

        if (!expectedCollectionNames.SequenceEqual(actualCollectionNames, StringComparer.Ordinal))
        {
            throw new DatabaseBackupException("Backup file does not contain the expected collection set for this application version.");
        }

        foreach (var (collectionName, documents) in payload.Collections)
        {
            foreach (var document in documents)
            {
                if (string.IsNullOrWhiteSpace(document.Id))
                {
                    throw new DatabaseBackupException($"Backup contains a document without an id in collection '{collectionName}'.");
                }

                if (document.Data.ValueKind is not JsonValueKind.Object)
                {
                    throw new DatabaseBackupException($"Backup document '{document.Id}' in collection '{collectionName}' must be a JSON object.");
                }
            }
        }
    }

    private static void EnsureSupportedBackupFormatVersion(int backupFormatVersion)
    {
        if (backupFormatVersion == CurrentBackupFormatVersion)
        {
            return;
        }

        throw new DatabaseBackupException($"Backup format version {backupFormatVersion} is not supported by this build. Add a backup format migration before importing it.");
    }

    private static void EnsureSupportedDatabaseSchemaVersion(int databaseSchemaVersion)
    {
        if (databaseSchemaVersion == CouchbaseLiteDatabaseProvider.CurrentDatabaseSchemaVersion)
        {
            return;
        }

        throw new DatabaseBackupException($"Database schema version {databaseSchemaVersion} is not supported by this build. Add a schema migration before using this backup.");
    }

    private static List<Document> GetDocuments(Collection collection, CancellationToken cancellationToken)
    {
        var query = QueryBuilder
            .Select(SelectResult.Expression(Meta.ID))
            .From(DataSource.Collection(collection))
            .OrderBy(Ordering.Expression(Meta.ID));

        var documents = new List<Document>();
        foreach (var result in query.Execute())
        {
            cancellationToken.ThrowIfCancellationRequested();

            var id = result.GetString(0);
            if (string.IsNullOrWhiteSpace(id))
            {
                continue;
            }

            var document = collection.GetDocument(id);
            if (document is not null)
            {
                documents.Add(document);
            }
        }

        return documents;
    }

    private static void ClearCollection(Collection collection)
    {
        foreach (var document in GetDocuments(collection, CancellationToken.None))
        {
            collection.Delete(document);
        }
    }

    private static JsonElement ParseJson(string json)
    {
        using var jsonDocument = JsonDocument.Parse(json);
        return jsonDocument.RootElement.Clone();
    }
}