using Journal.Core.Models;
using Journal.Core.Services.Database;
using Microsoft.Data.Sqlite;

namespace Journal.Core.Repositories;

public sealed class SqliteFragmentRepository(IDatabaseSessionService session) : IFragmentRepository
{
    private readonly IDatabaseSessionService _session = session;

    public List<Fragment> GetAll()
    {
        var conn = _session.GetConnection();
        return ReadAllFragments(conn);
    }

    public Fragment? GetById(Guid id)
    {
        var conn = _session.GetConnection();
        return ReadFragment(conn, id);
    }

    public void Add(Fragment fragment)
    {
        ArgumentNullException.ThrowIfNull(fragment);
        Normalize(fragment);
        var conn = _session.GetConnection();
        InsertFragment(conn, fragment);
    }

    public bool Remove(Guid id)
    {
        var conn = _session.GetConnection();
        return DeleteFragment(conn, id);
    }

    public bool Update(
        Guid id,
        string? type = null,
        string? description = null,
        IEnumerable<string>? tags = null,
        DateTimeOffset? time = null)
    {
        var conn = _session.GetConnection();
        var existing = ReadFragment(conn, id);
        if (existing is null)
            return false;

        if (type != null)
        {
            if (string.IsNullOrWhiteSpace(type))
                throw new ArgumentException("Type cannot be empty", nameof(type));
            existing.Type = type.Trim();
        }

        if (description != null)
        {
            if (string.IsNullOrWhiteSpace(description))
                throw new ArgumentException("Description cannot be empty", nameof(description));
            existing.Description = description.Trim();
        }

        if (tags != null)
        {
            existing.Tags = [..
                tags.Where(t => !string.IsNullOrWhiteSpace(t))
                    .Select(t => t.Trim())];
        }

        if (time.HasValue)
            existing.Time = time.Value;

        UpdateFragmentRow(conn, existing);
        return true;
    }

    public List<Fragment> GetByTag(string tag)
    {
        var q = tag?.Trim();
        if (string.IsNullOrWhiteSpace(q))
            return [];

        var conn = _session.GetConnection();
        var all = ReadAllFragments(conn);
        return [.. all.Where(f => f.Tags.Any(t => t.Contains(q, StringComparison.OrdinalIgnoreCase)))];
    }

    public List<Fragment> GetByType(string type)
    {
        var q = type?.Trim();
        if (string.IsNullOrWhiteSpace(q))
            return [];

        var conn = _session.GetConnection();
        var all = ReadAllFragments(conn);
        return [.. all.Where(f => f.Type.Contains(q, StringComparison.OrdinalIgnoreCase))];
    }

    public List<Fragment> Search(string? type = null, string? tag = null, DateTimeOffset? timeAfter = null)
    {
        var conn = _session.GetConnection();
        IEnumerable<Fragment> results = ReadAllFragments(conn);

        var qType = type?.Trim();
        var qTag = tag?.Trim();

        if (!string.IsNullOrWhiteSpace(qType))
            results = results.Where(f => f.Type.Contains(qType, StringComparison.OrdinalIgnoreCase));
        if (!string.IsNullOrWhiteSpace(qTag))
            results = results.Where(f => f.Tags.Any(t => t.Contains(qTag, StringComparison.OrdinalIgnoreCase)));
        if (timeAfter.HasValue)
            results = results.Where(f => f.Time > timeAfter.Value);

        return [.. results];
    }

    // ── Private helpers ──────────────────────────────────────────────

    private static void InsertFragment(SqliteConnection conn, Fragment f)
    {
        using var tx = conn.BeginTransaction();

        using var cmd = conn.CreateCommand();
        cmd.CommandText = """
            INSERT INTO fragments (guid, entry_id, type, description, time)
            VALUES (@guid, NULL, @type, @description, @time);
            """;
        cmd.Parameters.AddWithValue("@guid", f.Id.ToString("D"));
        cmd.Parameters.AddWithValue("@type", f.Type);
        cmd.Parameters.AddWithValue("@description", f.Description);
        cmd.Parameters.AddWithValue("@time", f.Time.ToString("O"));
        cmd.ExecuteNonQuery();

        var fragmentRowId = GetFragmentRowId(conn, f.Id);
        if (fragmentRowId.HasValue)
            InsertTags(conn, fragmentRowId.Value, f.Tags);

        tx.Commit();
    }

    private static void UpdateFragmentRow(SqliteConnection conn, Fragment f)
    {
        using var tx = conn.BeginTransaction();

        using var upd = conn.CreateCommand();
        upd.CommandText = """
            UPDATE fragments SET type = @type, description = @description, time = @time
            WHERE guid = @guid AND entry_id IS NULL;
            """;
        upd.Parameters.AddWithValue("@guid", f.Id.ToString("D"));
        upd.Parameters.AddWithValue("@type", f.Type);
        upd.Parameters.AddWithValue("@description", f.Description);
        upd.Parameters.AddWithValue("@time", f.Time.ToString("O"));
        upd.ExecuteNonQuery();

        var fragmentRowId = GetFragmentRowId(conn, f.Id);
        if (fragmentRowId.HasValue)
        {
            using var del = conn.CreateCommand();
            del.CommandText = "DELETE FROM fragment_tags WHERE fragment_id = @id;";
            del.Parameters.AddWithValue("@id", fragmentRowId.Value);
            del.ExecuteNonQuery();

            InsertTags(conn, fragmentRowId.Value, f.Tags);
        }

        tx.Commit();
    }

    private static bool DeleteFragment(SqliteConnection conn, Guid id)
    {
        using var tx = conn.BeginTransaction();

        var fragmentRowId = GetFragmentRowId(conn, id);
        if (fragmentRowId.HasValue)
        {
            using var delTags = conn.CreateCommand();
            delTags.CommandText = "DELETE FROM fragment_tags WHERE fragment_id = @id;";
            delTags.Parameters.AddWithValue("@id", fragmentRowId.Value);
            delTags.ExecuteNonQuery();
        }

        using var delFrag = conn.CreateCommand();
        delFrag.CommandText = "DELETE FROM fragments WHERE guid = @guid AND entry_id IS NULL;";
        delFrag.Parameters.AddWithValue("@guid", id.ToString("D"));
        var rows = delFrag.ExecuteNonQuery();

        tx.Commit();
        return rows > 0;
    }

    private static Fragment? ReadFragment(SqliteConnection conn, Guid id)
    {
        using var cmd = conn.CreateCommand();
        cmd.CommandText = """
            SELECT id, guid, type, description, time
            FROM fragments WHERE guid = @guid AND entry_id IS NULL;
            """;
        cmd.Parameters.AddWithValue("@guid", id.ToString("D"));

        using var reader = cmd.ExecuteReader();
        if (!reader.Read())
            return null;

        var fragment = MapRow(reader);
        fragment.Tags = ReadTags(conn, reader.GetInt64(0));
        return fragment;
    }

    private static List<Fragment> ReadAllFragments(SqliteConnection conn)
    {
        var fragments = new List<Fragment>();
        var rowIds = new List<long>();

        using var cmd = conn.CreateCommand();
        cmd.CommandText = """
            SELECT id, guid, type, description, time
            FROM fragments WHERE guid IS NOT NULL AND entry_id IS NULL
            ORDER BY time;
            """;

        using var reader = cmd.ExecuteReader();
        while (reader.Read())
        {
            fragments.Add(MapRow(reader));
            rowIds.Add(reader.GetInt64(0));
        }

        for (var i = 0; i < fragments.Count; i++)
            fragments[i].Tags = ReadTags(conn, rowIds[i]);

        return fragments;
    }

    private static List<string> ReadTags(SqliteConnection conn, long fragmentRowId)
    {
        using var cmd = conn.CreateCommand();
        cmd.CommandText = """
            SELECT t.name FROM tags t
            INNER JOIN fragment_tags ft ON ft.tag_id = t.id
            WHERE ft.fragment_id = @id
            ORDER BY t.name;
            """;
        cmd.Parameters.AddWithValue("@id", fragmentRowId);

        var tags = new List<string>();
        using var reader = cmd.ExecuteReader();
        while (reader.Read())
            tags.Add(reader.GetString(0));

        return tags;
    }

    private static long? GetFragmentRowId(SqliteConnection conn, Guid guid)
    {
        using var cmd = conn.CreateCommand();
        cmd.CommandText = "SELECT id FROM fragments WHERE guid = @guid;";
        cmd.Parameters.AddWithValue("@guid", guid.ToString("D"));
        var result = cmd.ExecuteScalar();
        return result is long id ? id : null;
    }

    private static void InsertTags(SqliteConnection conn, long fragmentRowId, List<string> tags)
    {
        if (tags.Count == 0) return;

        foreach (var tag in tags)
        {
            using var upsert = conn.CreateCommand();
            upsert.CommandText = "INSERT OR IGNORE INTO tags (name) VALUES (@name);";
            upsert.Parameters.AddWithValue("@name", tag);
            upsert.ExecuteNonQuery();

            using var getTagId = conn.CreateCommand();
            getTagId.CommandText = "SELECT id FROM tags WHERE name = @name;";
            getTagId.Parameters.AddWithValue("@name", tag);
            var tagId = (long)getTagId.ExecuteScalar()!;

            using var link = conn.CreateCommand();
            link.CommandText = "INSERT OR IGNORE INTO fragment_tags (fragment_id, tag_id) VALUES (@fid, @tid);";
            link.Parameters.AddWithValue("@fid", fragmentRowId);
            link.Parameters.AddWithValue("@tid", tagId);
            link.ExecuteNonQuery();
        }
    }

    private static Fragment MapRow(SqliteDataReader reader)
    {
        var guid = Guid.Parse(reader.GetString(1));
        var type = reader.GetString(2);
        var description = reader.IsDBNull(3) ? "" : reader.GetString(3);
        var time = reader.IsDBNull(4)
            ? DateTimeOffset.MinValue
            : DateTimeOffset.Parse(reader.GetString(4));
        return new Fragment(guid, type, description, time);
    }

    private static void Normalize(Fragment fragment)
    {
        fragment.Type = fragment.Type.Trim();
        fragment.Description = fragment.Description.Trim();
        fragment.Tags = [..
            fragment.Tags.Where(t => !string.IsNullOrWhiteSpace(t))
                .Select(t => t.Trim())];
    }
}
