// PRE-GENERATED (design session). Real SQLite profile store (M2). Requires Microsoft.Data.Sqlite.
// Implements IProfileDbService (+ Search/LoadEq/Upsert/ImportFromFolder for the sidebar/selection).
// Untested. Swap DesignProfileDbService -> this in the dev session when the DB is populated.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Data.Sqlite;
using DansoriEQ.Core.Eq;
using DansoriEQ.Core.Profiles;

namespace DansoriEQ.App.Profiles;

public sealed class SqliteProfileDbService : IProfileDbService
{
    private readonly string _connStr;
    private readonly string _dbPath;

    public SqliteProfileDbService(string dbPath)
    {
        _dbPath = dbPath;
        var dir = Path.GetDirectoryName(dbPath);
        if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir);
        _connStr = new SqliteConnectionStringBuilder { DataSource = dbPath }.ToString();
        EnsureSchema();
    }

    private SqliteConnection Open() { var c = new SqliteConnection(_connStr); c.Open(); return c; }

    private void EnsureSchema()
    {
        using var c = Open();
        using var cmd = c.CreateCommand();
        cmd.CommandText = @"
CREATE TABLE IF NOT EXISTS headphone(
  id INTEGER PRIMARY KEY,
  name TEXT NOT NULL, brand TEXT, type TEXT, source TEXT, license TEXT,
  distributable INTEGER, target TEXT, preamp_db REAL, filters_json TEXT, updated_at TEXT
);
CREATE UNIQUE INDEX IF NOT EXISTS ux_hp_name_src ON headphone(name, source);
CREATE INDEX IF NOT EXISTS ix_hp_type ON headphone(type);";
        cmd.ExecuteNonQuery();
    }

    // ---------- IProfileDbService ----------
    public DbInfo GetInfo()
    {
        using var c = Open();
        var info = new DbInfo
        {
            TotalProfiles = Scalar(c, "SELECT COUNT(*) FROM headphone"),
            IemCount = Scalar(c, "SELECT COUNT(*) FROM headphone WHERE type='iem'"),
            HeadphoneCount = Scalar(c, "SELECT COUNT(*) FROM headphone WHERE type='headphone'"),
            DbPath = _dbPath,
            SchemaVersion = 1,
            LastUpdatedUtc = DateTimeOffset.TryParse(ScalarStr(c, "SELECT MAX(updated_at) FROM headphone"), out var d) ? d : null,
            DbSizeBytes = File.Exists(_dbPath) ? new FileInfo(_dbPath).Length : 0
        };
        using var cmd = c.CreateCommand();
        cmd.CommandText = "SELECT source, license, MAX(distributable), COUNT(*) FROM headphone GROUP BY source, license";
        using var r = cmd.ExecuteReader();
        while (r.Read())
            info.Sources.Add(new DbSourceInfo
            {
                Name = r.IsDBNull(0) ? "(unknown)" : r.GetString(0),
                License = r.IsDBNull(1) ? "" : r.GetString(1),
                Distributable = !r.IsDBNull(2) && r.GetInt32(2) != 0,
                ProfileCount = r.GetInt32(3),
                Status = SourceStatus.UpToDate
            });
        return info;
    }

    public ProfileRef? FindMatch(string name, string? brand, string type)
    {
        if (string.IsNullOrWhiteSpace(name)) return null;
        using var c = Open();
        using var cmd = c.CreateCommand();
        cmd.CommandText = "SELECT id,name,type FROM headphone WHERE name=$n COLLATE NOCASE OR name LIKE $like LIMIT 1";
        cmd.Parameters.AddWithValue("$n", name);
        cmd.Parameters.AddWithValue("$like", "%" + name + "%");
        using var r = cmd.ExecuteReader();
        return r.Read() ? Ref(r) : null;
    }

    public async Task<DbInfo> UpdateAsync(IProgress<DbUpdateProgress>? progress = null, CancellationToken ct = default)
    {
        var tmp = Path.Combine(Path.GetTempPath(), "dansorieq_autoeq");
        Directory.CreateDirectory(tmp);
        var zip = Path.Combine(tmp, "autoeq.zip");

        // ── Step 1: Download with per-chunk progress (0 % → 40 %) ──
        progress?.Report(new DbUpdateProgress { Message = "AutoEq 저장소 연결 중…", Fraction = 0 });
        using (var http = new System.Net.Http.HttpClient { Timeout = TimeSpan.FromMinutes(60) })
        using (var resp = await http.GetAsync(
            "https://github.com/jaakkopasanen/AutoEq/archive/refs/heads/master.zip",
            System.Net.Http.HttpCompletionOption.ResponseHeadersRead, ct))
        {
            resp.EnsureSuccessStatusCode();
            long? total = resp.Content.Headers.ContentLength;

            await using var src = await resp.Content.ReadAsStreamAsync(ct);
            await using var dst = File.Create(zip);

            var buf = new byte[131072]; // 128 KB chunks
            long received = 0;
            int read;
            while ((read = await src.ReadAsync(buf, ct)) > 0)
            {
                await dst.WriteAsync(buf.AsMemory(0, read), ct);
                received += read;
                if (total > 0)
                {
                    var frac = (double)received / total.Value;
                    var mb   = received / 1_048_576.0;
                    var tmb  = total.Value / 1_048_576.0;
                    progress?.Report(new DbUpdateProgress
                    {
                        Message  = $"다운로드 {mb:0.0} / {tmb:0.0} MB",
                        Fraction = frac * 0.40   // 0 % → 40 %
                    });
                }
                else
                {
                    progress?.Report(new DbUpdateProgress
                    {
                        Message  = $"다운로드 중… {received / 1_048_576.0:0.0} MB",
                        Fraction = -1             // signals indeterminate
                    });
                }
            }
        }

        // ── Step 2: Extract (40 % → 55 %, shown as indeterminate) ──
        progress?.Report(new DbUpdateProgress { Message = "압축 해제 중… (시간이 걸릴 수 있습니다)", Fraction = -1 });
        var ex = Path.Combine(tmp, "extracted");
        await Task.Run(() =>
        {
            if (Directory.Exists(ex)) Directory.Delete(ex, true);
            System.IO.Compression.ZipFile.ExtractToDirectory(zip, ex);
        }, ct);

        // ── Step 3: Import with per-file progress (55 % → 95 %) ──
        progress?.Report(new DbUpdateProgress { Message = "프로파일 파싱 중…", Fraction = 0.55 });
        await Task.Run(() => ImportFromFolder(ex, "AutoEq", "mixed", true, progress), ct);

        progress?.Report(new DbUpdateProgress { Message = "완료 ✓", Fraction = 1.0 });
        return GetInfo();
    }

    // ---------- extra (sidebar / selection) ----------
    public IReadOnlyList<ProfileRef> Search(string? query, string? type)
    {
        using var c = Open();
        using var cmd = c.CreateCommand();
        cmd.CommandText = "SELECT id,name,type FROM headphone WHERE ($t IS NULL OR type=$t) AND ($q IS NULL OR name LIKE $like) ORDER BY name LIMIT 500";
        cmd.Parameters.AddWithValue("$t", (object?)type ?? DBNull.Value);
        cmd.Parameters.AddWithValue("$q", (object?)query ?? DBNull.Value);
        cmd.Parameters.AddWithValue("$like", "%" + (query ?? "") + "%");
        var list = new List<ProfileRef>();
        using var r = cmd.ExecuteReader();
        while (r.Read()) list.Add(Ref(r));
        return list;
    }

    public EqState? LoadEq(string id)
    {
        if (!long.TryParse(id, out var pk)) return null;
        using var c = Open();
        using var cmd = c.CreateCommand();
        cmd.CommandText = "SELECT preamp_db, filters_json FROM headphone WHERE id=$id";
        cmd.Parameters.AddWithValue("$id", pk);
        using var r = cmd.ExecuteReader();
        if (!r.Read()) return null;
        var s = new EqState { PreampDb = r.IsDBNull(0) ? 0 : r.GetDouble(0) };
        if (!r.IsDBNull(1)) DeserializeFilters(r.GetString(1), s);
        return s;
    }

    public void Upsert(string name, string? brand, string type, string source, string license, bool distributable, EqState eq)
    {
        using var c = Open();
        using var cmd = c.CreateCommand();
        cmd.CommandText = @"
INSERT INTO headphone(name,brand,type,source,license,distributable,preamp_db,filters_json,updated_at)
VALUES($name,$brand,$type,$source,$license,$dist,$pre,$filters,$now)
ON CONFLICT(name,source) DO UPDATE SET
  brand=$brand, type=$type, license=$license, distributable=$dist,
  preamp_db=$pre, filters_json=$filters, updated_at=$now;";
        cmd.Parameters.AddWithValue("$name", name);
        cmd.Parameters.AddWithValue("$brand", (object?)brand ?? DBNull.Value);
        cmd.Parameters.AddWithValue("$type", type);
        cmd.Parameters.AddWithValue("$source", source);
        cmd.Parameters.AddWithValue("$license", license);
        cmd.Parameters.AddWithValue("$dist", distributable ? 1 : 0);
        cmd.Parameters.AddWithValue("$pre", eq.PreampDb);
        cmd.Parameters.AddWithValue("$filters", SerializeFilters(eq));
        cmd.Parameters.AddWithValue("$now", DateTimeOffset.UtcNow.ToString("o"));
        cmd.ExecuteNonQuery();
    }

    /// <summary>Parse every "*ParametricEQ.txt" under dir (AutoEq layout) and upsert.</summary>
    public int ImportFromFolder(string dir, string source, string license, bool distributable, IProgress<DbUpdateProgress>? progress = null)
    {
        if (!Directory.Exists(dir)) return 0;
        var files = Directory.EnumerateFiles(dir, "*ParametricEQ.txt", SearchOption.AllDirectories).ToList();
        int n = 0;
        foreach (var f in files)
        {
            var name = Path.GetFileName(f).Replace(" ParametricEQ.txt", "").Replace("ParametricEQ.txt", "").Trim();
            if (name.Length == 0) continue;
            var eq = AutoEqParser.Parse(File.ReadAllText(f));
            Upsert(name, InferBrand(name), InferType(f, name), source, license, distributable, eq);
            n++;
            if (n % 50 == 0)
                progress?.Report(new DbUpdateProgress { Message = $"적재 {n}/{files.Count}", Fraction = 0.6 + 0.35 * n / Math.Max(1, files.Count) });
        }
        return n;
    }

    // ---------- helpers ----------
    private static ProfileRef Ref(SqliteDataReader r)
        => new() { Id = r.GetInt64(0).ToString(), Name = r.GetString(1), Type = r.IsDBNull(2) ? "iem" : r.GetString(2) };

    private static int Scalar(SqliteConnection c, string sql) { using var cmd = c.CreateCommand(); cmd.CommandText = sql; return Convert.ToInt32(cmd.ExecuteScalar() ?? 0); }
    private static string? ScalarStr(SqliteConnection c, string sql) { using var cmd = c.CreateCommand(); cmd.CommandText = sql; return cmd.ExecuteScalar() as string; }

    private sealed record FDto(string type, double fc, double gain, double q, bool enabled);

    private static string SerializeFilters(EqState s)
        => JsonSerializer.Serialize(s.Filters.Select(f => new FDto(f.Type.ToApoToken(), f.Fc, f.GainDb, f.Q, f.Enabled)));

    private static void DeserializeFilters(string json, EqState s)
    {
        var arr = JsonSerializer.Deserialize<List<FDto>>(json) ?? new();
        foreach (var d in arr)
            s.Filters.Add(new Filter { Type = FilterTypeExtensions.FromApoToken(d.type), Fc = d.fc, GainDb = d.gain, Q = d.q, Enabled = d.enabled });
    }

    private static string InferType(string path, string name)
    {
        var p = (path + " " + name).ToLowerInvariant();
        return (p.Contains("in-ear") || p.Contains("iem")) ? "iem" : "headphone";
    }

    private static string InferBrand(string name)
    {
        var i = name.IndexOf(' ');
        return i > 0 ? name.Substring(0, i) : name;
    }
}
