using System.Text.Json;
using Microsoft.Data.Sqlite;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.ExternalSystems;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Persistence;

namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Repositories;

/// <summary>
/// Site-side read-only implementation of <see cref="IExternalSystemRepository"/>
/// backed by the local SQLite database via <see cref="SiteStorageService"/>.
/// Write operations throw <see cref="NotSupportedException"/> because site-local
/// artifacts are managed exclusively through deployment from Central.
/// <para>
/// WP2.6a (arch-review misc): every <c>ExternalSystemDefinition</c>/<c>ExternalSystemMethod</c>
/// read is served from an <see cref="ExternalSystemDefinitionCache"/> snapshot instead of
/// a fresh SQLite scan per call — the by-ID lookups in particular used to re-fetch every
/// row (and, for methods, re-parse every system's JSON method list) just to find one
/// match. See <see cref="ExternalSystemDefinitionCache"/> for the sharing/invalidation
/// contract; <c>DeploymentManagerActor</c> invalidates it after an artifact deploy
/// applies external-system changes.
/// </para>
/// </summary>
public class SiteExternalSystemRepository : IExternalSystemRepository
{
    private readonly SiteStorageService _storage;
    private readonly ExternalSystemDefinitionCache _cache;

    /// <summary>
    /// Initializes a new site-side external system repository with a private,
    /// unshared cache — used by tests and any caller that wants each instance to see
    /// its own fresh view with no cross-instance sharing.
    /// </summary>
    /// <param name="storage">Storage service providing database access.</param>
    public SiteExternalSystemRepository(SiteStorageService storage)
        : this(storage, new ExternalSystemDefinitionCache())
    {
    }

    /// <summary>
    /// Initializes a new site-side external system repository backed by a shared
    /// <see cref="ExternalSystemDefinitionCache"/> — the production DI registration
    /// (<c>AddSiteRuntime</c>) injects one singleton cache shared by every scoped
    /// repository instance, so repeated resolutions (e.g. one per script execution's
    /// DI scope) do not each pay a fresh SQLite scan.
    /// </summary>
    /// <param name="storage">Storage service providing database access.</param>
    /// <param name="cache">The shared (or private) definition cache backing reads.</param>
    public SiteExternalSystemRepository(SiteStorageService storage, ExternalSystemDefinitionCache cache)
    {
        _storage = storage ?? throw new ArgumentNullException(nameof(storage));
        _cache = cache ?? throw new ArgumentNullException(nameof(cache));
    }

    // ── ExternalSystemDefinition (read) ──

    /// <inheritdoc />
    public async Task<IReadOnlyList<ExternalSystemDefinition>> GetAllExternalSystemsAsync(
        CancellationToken cancellationToken = default)
    {
        var snapshot = await _cache.GetOrLoadAsync(LoadSnapshotAsync, cancellationToken);
        return snapshot.ByName.Values.ToList();
    }

    /// <inheritdoc />
    public async Task<ExternalSystemDefinition?> GetExternalSystemByIdAsync(
        int id, CancellationToken cancellationToken = default)
    {
        var snapshot = await _cache.GetOrLoadAsync(LoadSnapshotAsync, cancellationToken);
        return snapshot.IdToName.TryGetValue(id, out var name)
            && snapshot.ByName.TryGetValue(name, out var definition)
            ? definition
            : null;
    }

    /// <inheritdoc />
    public async Task<ExternalSystemDefinition?> GetExternalSystemByNameAsync(
        string name, CancellationToken cancellationToken = default)
    {
        var snapshot = await _cache.GetOrLoadAsync(LoadSnapshotAsync, cancellationToken);
        return snapshot.ByName.TryGetValue(name, out var definition) ? definition : null;
    }

    // ── ExternalSystemMethod (read) ──

    /// <inheritdoc />
    public async Task<IReadOnlyList<ExternalSystemMethod>> GetMethodsByExternalSystemIdAsync(
        int externalSystemId, CancellationToken cancellationToken = default)
    {
        var snapshot = await _cache.GetOrLoadAsync(LoadSnapshotAsync, cancellationToken);
        if (!snapshot.IdToName.TryGetValue(externalSystemId, out var name))
            return Array.Empty<ExternalSystemMethod>();

        return snapshot.MethodsByName.TryGetValue(name, out var methods)
            ? methods
            : Array.Empty<ExternalSystemMethod>();
    }

    /// <inheritdoc />
    public async Task<ExternalSystemMethod?> GetMethodByNameAsync(
        int externalSystemId, string methodName, CancellationToken cancellationToken = default)
    {
        var methods = await GetMethodsByExternalSystemIdAsync(externalSystemId, cancellationToken);
        return methods.FirstOrDefault(
            m => m.Name.Equals(methodName, StringComparison.OrdinalIgnoreCase));
    }

    /// <inheritdoc />
    public async Task<ExternalSystemMethod?> GetExternalSystemMethodByIdAsync(
        int id, CancellationToken cancellationToken = default)
    {
        var snapshot = await _cache.GetOrLoadAsync(LoadSnapshotAsync, cancellationToken);
        if (!snapshot.MethodIdIndex.TryGetValue(id, out var key))
            return null;
        if (!snapshot.MethodsByName.TryGetValue(key.SystemName, out var methods))
            return null;

        return methods.FirstOrDefault(m => m.Id == id);
    }

    // ── DatabaseConnectionDefinition (read) ──

    /// <inheritdoc />
    public async Task<IReadOnlyList<DatabaseConnectionDefinition>> GetAllDatabaseConnectionsAsync(
        CancellationToken cancellationToken = default)
    {
        // Already open — SiteStorageService.CreateConnection now hands out a
        // LocalDb-managed connection. Opening it again would throw.
        await using var connection = CreateConnection();

        await using var command = connection.CreateCommand();
        command.CommandText = @"
            SELECT name, connection_string, max_retries, retry_delay_ms
            FROM database_connections";

        var results = new List<DatabaseConnectionDefinition>();
        await using var reader = await command.ExecuteReaderAsync(cancellationToken);
        while (await reader.ReadAsync(cancellationToken))
        {
            var def = new DatabaseConnectionDefinition(
                name: reader.GetString(0),
                connectionString: reader.GetString(1))
            {
                Id = GenerateSyntheticId(reader.GetString(0)),
                MaxRetries = reader.GetInt32(2),
                RetryDelay = TimeSpan.FromMilliseconds(reader.GetInt64(3))
            };
            results.Add(def);
        }

        return results;
    }

    /// <inheritdoc />
    public async Task<DatabaseConnectionDefinition?> GetDatabaseConnectionByIdAsync(
        int id, CancellationToken cancellationToken = default)
    {
        var all = await GetAllDatabaseConnectionsAsync(cancellationToken);
        return all.FirstOrDefault(d => d.Id == id);
    }

    /// <inheritdoc />
    public async Task<DatabaseConnectionDefinition?> GetDatabaseConnectionByNameAsync(
        string name, CancellationToken cancellationToken = default)
    {
        // Already open — SiteStorageService.CreateConnection now hands out a
        // LocalDb-managed connection. Opening it again would throw.
        await using var connection = CreateConnection();

        await using var command = connection.CreateCommand();
        command.CommandText = @"
            SELECT name, connection_string, max_retries, retry_delay_ms
            FROM database_connections
            WHERE name = @name";
        command.Parameters.AddWithValue("@name", name);

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

        return new DatabaseConnectionDefinition(
            name: reader.GetString(0),
            connectionString: reader.GetString(1))
        {
            Id = GenerateSyntheticId(reader.GetString(0)),
            MaxRetries = reader.GetInt32(2),
            RetryDelay = TimeSpan.FromMilliseconds(reader.GetInt64(3))
        };
    }

    // ── Write operations (not supported on site) ──

    /// <inheritdoc />
    public Task AddExternalSystemAsync(ExternalSystemDefinition definition, CancellationToken cancellationToken = default)
        => throw new NotSupportedException("Managed via artifact deployment from Central");

    /// <inheritdoc />
    public Task UpdateExternalSystemAsync(ExternalSystemDefinition definition, CancellationToken cancellationToken = default)
        => throw new NotSupportedException("Managed via artifact deployment from Central");

    /// <inheritdoc />
    public Task DeleteExternalSystemAsync(int id, CancellationToken cancellationToken = default)
        => throw new NotSupportedException("Managed via artifact deployment from Central");

    /// <inheritdoc />
    public Task AddExternalSystemMethodAsync(ExternalSystemMethod method, CancellationToken cancellationToken = default)
        => throw new NotSupportedException("Managed via artifact deployment from Central");

    /// <inheritdoc />
    public Task UpdateExternalSystemMethodAsync(ExternalSystemMethod method, CancellationToken cancellationToken = default)
        => throw new NotSupportedException("Managed via artifact deployment from Central");

    /// <inheritdoc />
    public Task DeleteExternalSystemMethodAsync(int id, CancellationToken cancellationToken = default)
        => throw new NotSupportedException("Managed via artifact deployment from Central");

    /// <inheritdoc />
    public Task AddDatabaseConnectionAsync(DatabaseConnectionDefinition definition, CancellationToken cancellationToken = default)
        => throw new NotSupportedException("Managed via artifact deployment from Central");

    /// <inheritdoc />
    public Task UpdateDatabaseConnectionAsync(DatabaseConnectionDefinition definition, CancellationToken cancellationToken = default)
        => throw new NotSupportedException("Managed via artifact deployment from Central");

    /// <inheritdoc />
    public Task DeleteDatabaseConnectionAsync(int id, CancellationToken cancellationToken = default)
        => throw new NotSupportedException("Managed via artifact deployment from Central");

    /// <inheritdoc />
    public Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
        => throw new NotSupportedException("Managed via artifact deployment from Central");

    // ── Private helpers ──

    /// <summary>
    /// Creates a new SQLite connection against the site database via
    /// <see cref="SiteStorageService.CreateConnection"/>. The
    /// connection string is owned by <see cref="SiteStorageService"/>; the repository
    /// no longer reaches into its private state via reflection.
    /// </summary>
    private SqliteConnection CreateConnection() => _storage.CreateConnection();

    private static ExternalSystemDefinition MapExternalSystem(SqliteDataReader reader)
    {
        var name = reader.GetString(0);
        return new ExternalSystemDefinition(
            name: name,
            endpointUrl: reader.GetString(1),
            authType: reader.GetString(2))
        {
            Id = GenerateSyntheticId(name),
            AuthConfiguration = reader.IsDBNull(3) ? null : reader.GetString(3),
            TimeoutSeconds = reader.IsDBNull(4) ? 0 : reader.GetInt32(4)
        };
    }

    /// <summary>
    /// Loads a full, fresh snapshot of every external system and its parsed method
    /// list in ONE query (name, endpoint config, and the <c>method_definitions</c> JSON
    /// column together) — the cache-miss path behind every
    /// <see cref="ExternalSystemDefinitionCache"/> read. Building both the by-name and
    /// by-ID indexes here, in a single pass, is what turns the by-ID lookups into O(1)
    /// dictionary gets instead of the prior full-table-then-LINQ-scan.
    /// </summary>
    private async Task<ExternalSystemDefinitionCache.Snapshot> LoadSnapshotAsync(
        CancellationToken cancellationToken)
    {
        // Already open — SiteStorageService.CreateConnection now hands out a
        // LocalDb-managed connection. Opening it again would throw.
        await using var connection = CreateConnection();

        await using var command = connection.CreateCommand();
        command.CommandText = @"
            SELECT name, endpoint_url, auth_type, auth_configuration, timeout_seconds, method_definitions
            FROM external_systems";

        var byName = new Dictionary<string, ExternalSystemDefinition>(StringComparer.Ordinal);
        var idToName = new Dictionary<int, string>();
        var methodsByName = new Dictionary<string, IReadOnlyList<ExternalSystemMethod>>(StringComparer.Ordinal);
        var methodIdIndex = new Dictionary<int, (string SystemName, string MethodName)>();

        await using var reader = await command.ExecuteReaderAsync(cancellationToken);
        while (await reader.ReadAsync(cancellationToken))
        {
            var definition = MapExternalSystem(reader);
            byName[definition.Name] = definition;
            idToName[definition.Id] = definition.Name;

            var methodsJson = reader.IsDBNull(5) ? null : reader.GetString(5);
            var methods = string.IsNullOrWhiteSpace(methodsJson)
                ? Array.Empty<ExternalSystemMethod>()
                : ParseMethodDefinitions(methodsJson, definition.Id);
            methodsByName[definition.Name] = methods;
            foreach (var method in methods)
                methodIdIndex[method.Id] = (definition.Name, method.Name);
        }

        return new ExternalSystemDefinitionCache.Snapshot(byName, idToName, methodsByName, methodIdIndex);
    }

    // Built ONCE per assembly (Phase 1, arch-review misc): JsonSerializerOptions
    // construction is not free and every method-list parse used to mint a new
    // instance — now shared across every LoadSnapshotAsync call.
    private static readonly JsonSerializerOptions MethodDefinitionsJsonOptions =
        new() { PropertyNameCaseInsensitive = true };

    private static IReadOnlyList<ExternalSystemMethod> ParseMethodDefinitions(
        string json, int externalSystemId)
    {
        try
        {
            var methods = JsonSerializer.Deserialize<List<MethodDefinitionDto>>(json,
                MethodDefinitionsJsonOptions);

            if (methods is null)
                return Array.Empty<ExternalSystemMethod>();

            return methods.Select(m => new ExternalSystemMethod(
                name: m.Name ?? string.Empty,
                httpMethod: m.HttpMethod ?? "GET",
                path: m.Path ?? string.Empty)
            {
                Id = GenerateSyntheticId($"{externalSystemId}:{m.Name}"),
                ExternalSystemDefinitionId = externalSystemId,
                ParameterDefinitions = m.ParameterDefinitions,
                ReturnDefinition = m.ReturnDefinition
            }).ToList();
        }
        catch (JsonException)
        {
            return Array.Empty<ExternalSystemMethod>();
        }
    }

    /// <summary>
    /// Generates a stable positive integer ID from a string name.
    /// Uses a deterministic FNV-1a hash rather than <see cref="string.GetHashCode()"/>,
    /// which is randomized per process on .NET Core and would therefore change every
    /// time the process restarts — breaking any caller that stored an ID and later
    /// looks the entity up by that ID.
    /// </summary>
    private static int GenerateSyntheticId(string name) => SyntheticId.From(name);

    /// <summary>
    /// DTO for deserializing individual method entries from the method_definitions JSON column.
    /// </summary>
    private sealed class MethodDefinitionDto
    {
        /// <summary>
        /// Name of the external system method.
        /// </summary>
        public string? Name { get; set; }

        /// <summary>
        /// HTTP method (GET, POST, PUT, DELETE, etc.) for the API call.
        /// </summary>
        public string? HttpMethod { get; set; }

        /// <summary>
        /// Path component of the endpoint URL.
        /// </summary>
        public string? Path { get; set; }

        /// <summary>
        /// JSON-serialized parameter definitions for the method.
        /// </summary>
        public string? ParameterDefinitions { get; set; }

        /// <summary>
        /// JSON-serialized return value definition for the method.
        /// </summary>
        public string? ReturnDefinition { get; set; }
    }
}
