using System.Formats.Tar;
using System.IO.Compression;
using System.Runtime.InteropServices;
using System.Text.Json.Nodes;
using Microsoft.Data.Sqlite;

namespace {{ namespace }}.Data;

/// <summary>
/// Single data-access layer against mcp_store.db — both the relational
/// `endpoints` table and the `semantic_endpoints` vec0 virtual table
/// (REQ-2.4.1's single-store consolidation). This class never creates
/// tables — mcpify's shared pipeline (architecture.md §1, steps 5/6)
/// already wrote mcp_store.db's schema before any generated code runs.
/// Mirrors `targets::python::templates::data::store.py`'s query shapes.
///
/// sqlite-vec ships no NuGet wrapper (unlike Rust's `sqlite-vec` crate or
/// Python's `sqlite-vec` PyPI package, both of which bundle the native
/// extension) — the loadable `vec0` native library is expected alongside
/// the built assembly (`{{ tool_prefix_env }}_SQLITE_VEC_PATH` overrides
/// the path). When that override isn't set and no `vec0` library sits next
/// to the assembly already, it's downloaded from sqlite-vec's GitHub
/// releases on first use and cached there — the same on-first-use fetch
/// this target's `Services/EmbeddingService.cs` already does for its ONNX
/// model, so both the local `dotnet run` flow and the Dockerfile's
/// `populate-embeddings` build step work without any manual setup.
/// </summary>
public sealed class SqliteVecStore(string dbPath = "mcp_store.db")
{
    private const string Columns =
        "operation_id, path, method, summary, description, input_schema, output_schema, auth_scheme_ref";

    private const string SqliteVecVersion = "0.1.9";

    // mcpify:versions:begin
    private static readonly Dictionary<string, string> VersionStoreFiles = new()
    {
{%- for v in version_entries %}
        ["{{ v.label }}"] = "{{ v.db_file }}",
{%- endfor %}
    };
    // mcpify:versions:end

    /// <summary>
    /// Resolves the active `ApiVersion` (from the config cascade) to its
    /// store file — the default version's data always lives in the
    /// project-root `mcp_store.db` this target has always used; every
    /// other version lives in its own `mcp_store_v&lt;label&gt;.db` sibling,
    /// added by a later `mcpify add-version` call.
    /// </summary>
    public static string ResolveStorePath(string apiVersion) =>
        VersionStoreFiles.TryGetValue(apiVersion, out var fileName)
            ? fileName
            : throw new ArgumentException(
                $"unknown api_version '{apiVersion}' — run the 'versions' command to see what's available");

    /// <summary>Every store file this project supports — the full set
    /// `PopulateEmbeddingsService`'s `--all` backfill walks, one
    /// independent `semantic_endpoints` table per version.</summary>
    public static IReadOnlyCollection<string> AllStoreFiles => VersionStoreFiles.Values;

    private readonly Lazy<string> _vecExtensionPath = new(ResolveExtensionPath);

    private SqliteConnection? _cachedConnection;
    private readonly Lock _cachedConnectionLock = new();

    /// <summary>
    /// Process-wide (well, instance-wide — <c>Core/DataStore.cs</c>
    /// registers exactly one <see cref="SqliteVecStore"/> as a DI
    /// singleton), in-memory-backed connection: built once, on first
    /// access, by opening the on-disk file read-only and copying its
    /// entire contents into a fresh <c>:memory:</c> connection via
    /// SQLite's backup API, then disposing the on-disk connection
    /// immediately — so its file handle/lock is held only for that brief
    /// copy, not for the process's lifetime. This means an external
    /// process (a fresh <c>populate-embeddings</c> run, a deployment
    /// replacing the file) can update `mcp_store.db` without hitting
    /// "database is locked" against a live server, at the cost of the
    /// running process only picking up such an update on its next
    /// restart.
    ///
    /// Not a field-initialized <see cref="Lazy{T}"/> (unlike
    /// <see cref="_vecExtensionPath"/>): a field initializer can't call an
    /// instance method or reference another instance field (CS0236), and
    /// building this connection needs both <c>dbPath</c> (via
    /// <see cref="OpenReadOnly"/>) and <see cref="_vecExtensionPath"/>.
    /// </summary>
    private SqliteConnection CachedConnection()
    {
        lock (_cachedConnectionLock)
        {
            return _cachedConnection ??= BuildCachedConnection();
        }
    }

    private SqliteConnection BuildCachedConnection()
    {
        using var diskConnection = OpenReadOnly();

        var memConnection = new SqliteConnection(new SqliteConnectionStringBuilder { DataSource = ":memory:" }.ToString());
        memConnection.Open();
        memConnection.EnableExtensions(true);
        memConnection.LoadExtension(_vecExtensionPath.Value);

        diskConnection.BackupDatabase(memConnection);
        return memConnection;
    }

    /// <summary>Every caller except <see cref="OpenReadWrite"/>'s one consumer (`Services/PopulateEmbeddingsService.cs`) uses this.</summary>
    public SqliteConnection OpenReadOnly() => Open(SqliteOpenMode.ReadOnly);

    /// <summary>Read-write counterpart — the only code path that writes to `semantic_endpoints`.</summary>
    public SqliteConnection OpenReadWrite() => Open(SqliteOpenMode.ReadWriteCreate);

    public EndpointRecord? GetEndpoint(string operationId)
    {
        var connection = CachedConnection();
        using var command = connection.CreateCommand();
        command.CommandText = $"SELECT {Columns} FROM endpoints WHERE operation_id = $operationId";
        command.Parameters.AddWithValue("$operationId", operationId);

        using var reader = command.ExecuteReader();
        return reader.Read() ? ReadEndpoint(reader) : null;
    }

    public IReadOnlyList<EndpointRecord> ListEndpoints()
    {
        var connection = CachedConnection();
        using var command = connection.CreateCommand();
        command.CommandText = $"SELECT {Columns} FROM endpoints";

        using var reader = command.ExecuteReader();
        var results = new List<EndpointRecord>();
        while (reader.Read())
        {
            results.Add(ReadEndpoint(reader));
        }
        return results;
    }

    /// <summary>
    /// k-nearest-neighbor search over `semantic_endpoints` (sqlite-vec's
    /// documented `MATCH ... AND k = ...` query form). <paramref name="queryEmbedding"/>
    /// must come from the same model as the vectors it's compared
    /// against — see <c>Services/EmbeddingService.cs</c>.
    ///
    /// <c>Similarity</c> is a simple `1 - distance` transform of
    /// sqlite-vec's L2 distance, not a true cosine-similarity
    /// computation; since embeddings are normalized (unit vectors),
    /// L2-distance ordering already matches cosine-similarity ordering,
    /// so result *ranking* is correct even though the displayed score is
    /// an approximation.
    /// </summary>
    public IReadOnlyList<SearchResult> SearchEndpoints(float[] queryEmbedding, int limit)
    {
        var connection = CachedConnection();
        using var command = connection.CreateCommand();
        command.CommandText =
            "SELECT e.operation_id, e.summary, s.distance " +
            "FROM semantic_endpoints s " +
            "JOIN endpoints e ON e.operation_id = s.operation_id " +
            "WHERE s.embedding MATCH $query AND k = $limit " +
            "ORDER BY s.distance";
        command.Parameters.AddWithValue("$query", SerializeFloat32(queryEmbedding));
        command.Parameters.AddWithValue("$limit", limit);

        using var reader = command.ExecuteReader();
        var results = new List<SearchResult>();
        while (reader.Read())
        {
            var distance = reader.GetDouble(2);
            results.Add(new SearchResult(
                reader.GetString(0),
                reader.IsDBNull(1) ? null : reader.GetString(1),
                1.0 - distance));
        }
        return results;
    }

    /// <summary>Serializes a float vector into sqlite-vec's expected little-endian `float[]` BLOB wire format.</summary>
    public static byte[] SerializeFloat32(float[] vector)
    {
        var bytes = new byte[vector.Length * sizeof(float)];
        Buffer.BlockCopy(vector, 0, bytes, 0, bytes.Length);
        return bytes;
    }

    private SqliteConnection Open(SqliteOpenMode mode)
    {
        var connectionString = new SqliteConnectionStringBuilder { DataSource = dbPath, Mode = mode }.ToString();
        var connection = new SqliteConnection(connectionString);
        connection.Open();
        connection.EnableExtensions(true);
        connection.LoadExtension(_vecExtensionPath.Value);
        return connection;
    }

    /// <summary>Resolves the path <see cref="Open"/> passes to <c>LoadExtension</c> — SQLite appends the platform-specific suffix (.dylib/.so/.dll) itself, so this stays extension-less.</summary>
    private static string ResolveExtensionPath()
    {
        var overridePath = Environment.GetEnvironmentVariable("{{ tool_prefix_env }}_SQLITE_VEC_PATH");
        if (overridePath is not null)
        {
            return overridePath;
        }

        var basePath = Path.Combine(AppContext.BaseDirectory, "vec0");
        if (!File.Exists(basePath + NativeLibraryExtension()))
        {
            DownloadSqliteVecExtension(AppContext.BaseDirectory);
        }
        return basePath;
    }

    private static string NativeLibraryExtension() =>
        OperatingSystem.IsWindows() ? ".dll" : OperatingSystem.IsMacOS() ? ".dylib" : ".so";

    private static void DownloadSqliteVecExtension(string destinationDir)
    {
        var assetName = ReleaseAssetName();
        var url = $"https://github.com/asg017/sqlite-vec/releases/download/v{SqliteVecVersion}/{assetName}";

        using var client = new HttpClient();
        using var response = client.GetAsync(url).GetAwaiter().GetResult();
        response.EnsureSuccessStatusCode();
        using var responseStream = response.Content.ReadAsStream();
        using var gzip = new GZipStream(responseStream, CompressionMode.Decompress);
        using var tar = new TarReader(gzip);

        Directory.CreateDirectory(destinationDir);
        while (tar.GetNextEntry() is { EntryType: TarEntryType.RegularFile } entry)
        {
            var destinationPath = Path.Combine(destinationDir, Path.GetFileName(entry.Name));
            var tempPath = $"{destinationPath}.download";
            using (var file = File.Create(tempPath))
            {
                entry.DataStream!.CopyTo(file);
            }
            File.Move(tempPath, destinationPath, overwrite: true);
        }
    }

    /// <summary>Maps the running platform to one of sqlite-vec's published `loadable-&lt;os&gt;-&lt;arch&gt;` release assets.</summary>
    private static string ReleaseAssetName()
    {
        var os = OperatingSystem.IsWindows() ? "windows" : OperatingSystem.IsMacOS() ? "macos" : "linux";
        var arch = RuntimeInformation.ProcessArchitecture switch
        {
            Architecture.Arm64 => "aarch64",
            Architecture.X64 => "x86_64",
            var other => throw new PlatformNotSupportedException(
                $"sqlite-vec publishes no prebuilt loadable extension for {os}/{other}; " +
                $"set {{ tool_prefix_env }}_SQLITE_VEC_PATH to a locally built vec0 library instead."),
        };
        return $"sqlite-vec-{SqliteVecVersion}-loadable-{os}-{arch}.tar.gz";
    }

    private static EndpointRecord ReadEndpoint(SqliteDataReader reader) => new(
        reader.GetString(0),
        reader.GetString(1),
        reader.GetString(2),
        reader.IsDBNull(3) ? null : reader.GetString(3),
        reader.IsDBNull(4) ? null : reader.GetString(4),
        JsonNode.Parse(reader.GetString(5)),
        JsonNode.Parse(reader.GetString(6)),
        reader.IsDBNull(7) ? null : reader.GetString(7));
}
