using System.Security.Cryptography;
using System.Text;
using System.Globalization;
using Journal.Core.Dtos;
using Journal.Core.Services.Config;
using Microsoft.Data.Sqlite;

namespace Journal.Core.Services.Database;

public sealed class JournalDatabaseService(IJournalConfigService config) : IJournalDatabaseService
{
    public const int KeySize = 32;
    public const int Iterations = 600_000;
    private static readonly byte[] DatabaseKeySalt = Encoding.UTF8.GetBytes("a_fixed_salt_for_the_db_key_deriv");
    private static readonly Lock SqliteInitLock = new();
    private static bool _sqliteInitialized;
    private static readonly IReadOnlyList<string> RequiredSchemaTables =
        ["entries", "sections", "fragments", "tags", "fragment_tags", "lists", "todo_lists", "todo_items", "entry_documents", "conversations", "conversation_messages"];

    private readonly IJournalConfigService _config = config;

    public string GetDatabasePath()
    {
        var directory = ResolveDatabaseDirectory();

        Directory.CreateDirectory(directory);
        return Path.GetFullPath(Path.Combine(directory, _config.Current.DatabaseFilename));
    }

    public byte[] DeriveDatabaseKey(string password)
    {
        if (string.IsNullOrWhiteSpace(password))
            throw new ArgumentException("Password cannot be empty.", nameof(password));

        return Rfc2898DeriveBytes.Pbkdf2(
            Encoding.UTF8.GetBytes(password),
            DatabaseKeySalt,
            Iterations,
            HashAlgorithmName.SHA256,
            KeySize);
    }

    public string BuildPragmaKeyStatement(string password)
    {
        var dbKeyHex = Convert.ToHexString(DeriveDatabaseKey(password)).ToLowerInvariant();
        return $"PRAGMA key = \"x'{dbKeyHex}'\"";
    }

    public IReadOnlyDictionary<string, string> GetSchemaStatements()
    {
        return new Dictionary<string, string>(StringComparer.Ordinal)
        {
            ["entries"] = """
                CREATE TABLE IF NOT EXISTS entries (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    date TEXT NOT NULL UNIQUE
                );
                """,
            ["sections"] = """
                CREATE TABLE IF NOT EXISTS sections (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    entry_id INTEGER NOT NULL,
                    title TEXT NOT NULL,
                    content TEXT,
                    FOREIGN KEY (entry_id) REFERENCES entries (id)
                );
                """,
            ["fragments"] = """
                CREATE TABLE IF NOT EXISTS fragments (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    guid TEXT UNIQUE,
                    entry_id INTEGER,
                    type TEXT NOT NULL,
                    description TEXT,
                    time TEXT,
                    FOREIGN KEY (entry_id) REFERENCES entries (id)
                );
                """,
            ["tags"] = """
                CREATE TABLE IF NOT EXISTS tags (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    name TEXT NOT NULL UNIQUE
                );
                """,
            ["fragment_tags"] = """
                CREATE TABLE IF NOT EXISTS fragment_tags (
                    fragment_id INTEGER NOT NULL,
                    tag_id INTEGER NOT NULL,
                    PRIMARY KEY (fragment_id, tag_id),
                    FOREIGN KEY (fragment_id) REFERENCES fragments (id),
                    FOREIGN KEY (tag_id) REFERENCES tags (id)
                );
                """,
            ["lists"] = """
                CREATE TABLE IF NOT EXISTS lists (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    guid TEXT UNIQUE,
                    label TEXT NOT NULL,
                    content TEXT,
                    created_at TEXT,
                    updated_at TEXT
                );
                """,
            ["todo_lists"] = """
                CREATE TABLE IF NOT EXISTS todo_lists (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    guid TEXT UNIQUE,
                    label TEXT NOT NULL,
                    created_at TEXT
                );
                """,
            ["todo_items"] = """
                CREATE TABLE IF NOT EXISTS todo_items (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    guid TEXT UNIQUE,
                    list_id INTEGER NOT NULL,
                    text TEXT NOT NULL,
                    done INTEGER DEFAULT 0,
                    sort_order INTEGER DEFAULT 0,
                    FOREIGN KEY (list_id) REFERENCES todo_lists (id)
                );
                """,
            ["entry_documents"] = """
                CREATE TABLE IF NOT EXISTS entry_documents (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    guid TEXT UNIQUE,
                    file_name TEXT NOT NULL UNIQUE,
                    content TEXT NOT NULL,
                    is_template INTEGER NOT NULL DEFAULT 0,
                    updated_at TEXT
                );
                """,
            ["conversations"] = """
                CREATE TABLE IF NOT EXISTS conversations (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    guid TEXT UNIQUE,
                    title TEXT NOT NULL,
                    created_at TEXT,
                    updated_at TEXT
                );
                """,
            ["conversation_messages"] = """
                CREATE TABLE IF NOT EXISTS conversation_messages (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    guid TEXT UNIQUE,
                    conversation_id INTEGER NOT NULL,
                    role TEXT NOT NULL,
                    text TEXT NOT NULL,
                    created_at TEXT,
                    FOREIGN KEY (conversation_id) REFERENCES conversations (id)
                );
                """
        };
    }

    public JournalDatabaseStatus GetStatus(string password)
    {
        var tables = GetSchemaStatements().Keys.OrderBy(x => x, StringComparer.Ordinal).ToArray();
        var (Ready, Message) = ProbeRuntime(password);
        return new JournalDatabaseStatus(
            DatabasePath: GetDatabasePath(),
            KeyLengthBytes: DeriveDatabaseKey(password).Length,
            Iterations: Iterations,
            KeyDerivation: "PBKDF2-HMAC-SHA256",
            SchemaTables: tables,
            RuntimeReady: Ready,
            RuntimeMessage: Message);
    }

    public JournalDatabaseHydrationResult HydrateWorkspace(string password)
    {
        using var connection = OpenEncryptedConnection(password);
        EnsureSchema(connection);
        var runtimeReady = HasRequiredTables(connection);

        var entryDocumentsProcessed = CountEntryDocuments(connection);

        return new JournalDatabaseHydrationResult(
            DatabasePath: GetDatabasePath(),
            EntryFilesProcessed: entryDocumentsProcessed,
            RuntimeReady: runtimeReady,
            Message: runtimeReady
                ? "Workspace hydration completed with SQLCipher runtime schema validation and document store readiness."
                : "Workspace hydration completed, but required SQLCipher schema tables were not found.");
    }

    private static void EnsureSqliteInitialized()
    {
        if (_sqliteInitialized)
            return;

        lock (SqliteInitLock)
        {
            if (_sqliteInitialized)
                return;

            SQLitePCL.Batteries_V2.Init();
            _sqliteInitialized = true;
        }
    }

    public SqliteConnection OpenEncryptedConnection(string password)
    {
        if (string.IsNullOrWhiteSpace(password))
            throw new ArgumentException("Password cannot be empty.", nameof(password));

        EnsureSqliteInitialized();

        var connection = new SqliteConnection($"Data Source={GetDatabasePath()};Mode=ReadWriteCreate;Pooling=False");
        connection.Open();

        using var keyCmd = connection.CreateCommand();
        keyCmd.CommandText = BuildPragmaKeyStatement(password) + ";";
        keyCmd.ExecuteNonQuery();

        using var verifyCmd = connection.CreateCommand();
        verifyCmd.CommandText = "SELECT count(*) FROM sqlite_master;";
        _ = verifyCmd.ExecuteScalar();

        return connection;
    }

    public void EnsureSchema(SqliteConnection connection)
    {
        foreach (var statement in GetSchemaStatements().Values)
        {
            using var cmd = connection.CreateCommand();
            cmd.CommandText = statement;
            cmd.ExecuteNonQuery();
        }
    }

    private static bool HasRequiredTables(SqliteConnection connection)
    {
        var existing = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        using var cmd = connection.CreateCommand();
        cmd.CommandText = "SELECT name FROM sqlite_master WHERE type='table'";
        using var reader = cmd.ExecuteReader();
        while (reader.Read())
        {
            if (!reader.IsDBNull(0))
                existing.Add(reader.GetString(0));
        }

        return RequiredSchemaTables.All(existing.Contains);
    }

    private (bool Ready, string Message) ProbeRuntime(string password)
    {
        try
        {
            using var connection = OpenEncryptedConnection(password);
            EnsureSchema(connection);
            var ready = HasRequiredTables(connection);
            return ready
                ? (true, "SQLCipher runtime is available and schema tables are present.")
                : (false, "SQLCipher runtime opened, but required schema tables are missing.");
        }
        catch (Exception ex)
        {
            return (false, $"SQLCipher runtime check failed: {ex.Message}");
        }
    }

    private static int CountEntryDocuments(SqliteConnection connection)
    {
        using var cmd = connection.CreateCommand();
        cmd.CommandText = "SELECT COUNT(*) FROM entry_documents;";
        var scalar = cmd.ExecuteScalar();
        if (scalar is null || scalar is DBNull)
            return 0;

        return Convert.ToInt32(scalar, CultureInfo.InvariantCulture);
    }

    private string ResolveDatabaseDirectory()
    {
        var overrideDir = Environment.GetEnvironmentVariable("JOURNAL_DATABASE_DIR");
        if (!string.IsNullOrWhiteSpace(overrideDir))
            return Path.GetFullPath(overrideDir);

        return Path.GetFullPath(Path.Combine(_config.Current.VaultDirectory, "db"));
    }

}
