using System.Diagnostics;

namespace Flo.Installer.Provisioning;

internal sealed class PostgresProvisioner(Paths paths)
{
    public bool IsInitialized() => File.Exists(Path.Combine(paths.PostgresData, "PG_VERSION"));

    public void Initialize(string superuser, string superuserPassword)
    {
        Directory.CreateDirectory(paths.PostgresData);

        string pwFile = Path.Combine(paths.DataRoot, ".pg-init-pwd.tmp");
        File.WriteAllText(pwFile, superuserPassword);
        try
        {
            RunToCompletion(paths.PostgresBin("initdb"),
                ["--pgdata", paths.PostgresData,
                 "--username", superuser,
                 "--pwfile", pwFile,
                 "--auth-local", "scram-sha-256",
                 "--auth-host", "scram-sha-256",
                 "--encoding", "UTF8"],
                env: null);
        }
        finally
        {
            if (File.Exists(pwFile)) File.Delete(pwFile);
        }
    }

    public void ConfigurePort(int port)
    {
        string confPath = Path.Combine(paths.PostgresData, "postgresql.conf");
        var lines = File.ReadAllLines(confPath).ToList();
        SetOrAppend(lines, "listen_addresses", "'127.0.0.1'");
        SetOrAppend(lines, "port", port.ToString());
        File.WriteAllLines(confPath, lines);
    }

    public ProcessRunner Start(int port) =>
        ProcessRunner.Start(paths.PostgresBin("postgres"),
            ["-D", paths.PostgresData, "-p", port.ToString()],
            env: null,
            name: "postgres");

    // pg_ctl stop -m fast is the portable graceful shutdown — faster than smart, cleaner than SIGKILL.
    public void StopGracefully()
    {
        RunToCompletion(paths.PostgresBin("pg_ctl"),
            ["stop", "-D", paths.PostgresData, "-m", "fast", "-w"],
            env: null);
    }

    public void EnsureRoleAndDatabase(
        int port,
        string superuser,
        string superuserPassword,
        string role,
        string rolePassword,
        string database)
    {
        if (!RoleExists(port, superuser, superuserPassword, role))
        {
            Psql(port, superuser, superuserPassword, "postgres",
                $"CREATE ROLE \"{role}\" LOGIN PASSWORD '{EscapeLiteral(rolePassword)}';");
        }

        if (!DatabaseExists(port, superuser, superuserPassword, database))
        {
            Psql(port, superuser, superuserPassword, "postgres",
                $"CREATE DATABASE \"{database}\" OWNER \"{role}\";");
        }
    }

    private bool RoleExists(int port, string user, string password, string role)
        => PsqlScalar(port, user, password, "postgres",
            $"SELECT 1 FROM pg_roles WHERE rolname = '{EscapeLiteral(role)}';") == "1";

    private bool DatabaseExists(int port, string user, string password, string database)
        => PsqlScalar(port, user, password, "postgres",
            $"SELECT 1 FROM pg_database WHERE datname = '{EscapeLiteral(database)}';") == "1";

    private void Psql(int port, string user, string password, string db, string sql)
    {
        var (exit, stdout, stderr) = InvokePsql(port, user, password, db, sql, quiet: false);
        if (exit != 0)
            throw new InvalidOperationException($"psql failed (exit {exit})\nstdout: {stdout}\nstderr: {stderr}");
    }

    private string PsqlScalar(int port, string user, string password, string db, string sql)
    {
        var (exit, stdout, stderr) = InvokePsql(port, user, password, db, sql, quiet: true);
        if (exit != 0)
            throw new InvalidOperationException($"psql scalar failed (exit {exit})\nstdout: {stdout}\nstderr: {stderr}");
        return stdout.Trim();
    }

    private (int exit, string stdout, string stderr) InvokePsql(
        int port, string user, string password, string db, string sql, bool quiet)
    {
        var psi = new ProcessStartInfo(paths.PostgresBin("psql"))
        {
            UseShellExecute = false,
            RedirectStandardOutput = true,
            RedirectStandardError = true,
            CreateNoWindow = true,
        };
        psi.ArgumentList.Add("-h"); psi.ArgumentList.Add("127.0.0.1");
        psi.ArgumentList.Add("-p"); psi.ArgumentList.Add(port.ToString());
        psi.ArgumentList.Add("-U"); psi.ArgumentList.Add(user);
        psi.ArgumentList.Add("-d"); psi.ArgumentList.Add(db);
        psi.ArgumentList.Add("-v"); psi.ArgumentList.Add("ON_ERROR_STOP=1");
        if (quiet)
        {
            psi.ArgumentList.Add("-At"); // unaligned, tuples-only
        }
        psi.ArgumentList.Add("-c"); psi.ArgumentList.Add(sql);
        psi.Environment["PGPASSWORD"] = password;

        using var proc = Process.Start(psi) ?? throw new InvalidOperationException("Failed to start psql");
        string stdout = proc.StandardOutput.ReadToEnd();
        string stderr = proc.StandardError.ReadToEnd();
        proc.WaitForExit();
        return (proc.ExitCode, stdout, stderr);
    }

    private static string EscapeLiteral(string s) => s.Replace("'", "''");

    private static void SetOrAppend(List<string> lines, string key, string value)
    {
        string prefix = key;
        int i = lines.FindIndex(l =>
        {
            string t = l.TrimStart();
            return t.StartsWith(prefix + " ") || t.StartsWith(prefix + "\t") || t.StartsWith(prefix + "=");
        });
        string newLine = $"{key} = {value}";
        if (i >= 0) lines[i] = newLine;
        else lines.Add(newLine);
    }

    private static void RunToCompletion(string exe, IReadOnlyList<string> args, IReadOnlyDictionary<string, string>? env)
    {
        var psi = new ProcessStartInfo(exe)
        {
            UseShellExecute = false,
            RedirectStandardOutput = true,
            RedirectStandardError = true,
            CreateNoWindow = true,
        };
        foreach (var a in args) psi.ArgumentList.Add(a);
        if (env is not null)
            foreach (var kv in env) psi.Environment[kv.Key] = kv.Value;

        using var proc = Process.Start(psi) ?? throw new InvalidOperationException($"Failed to start {exe}");
        string stdout = proc.StandardOutput.ReadToEnd();
        string stderr = proc.StandardError.ReadToEnd();
        proc.WaitForExit();
        if (proc.ExitCode != 0)
            throw new InvalidOperationException(
                $"{Path.GetFileName(exe)} failed (exit {proc.ExitCode})\nstdout: {stdout}\nstderr: {stderr}");
    }
}
