using System.Security.Cryptography;
using Microsoft.Data.Sqlite;

// VelaPanel admin account recovery tool.
// Retrieves panel user details and resets username / email / password
// directly against panel.db, writing an ASP.NET Core Identity v3 password hash.

const string DbPath = @"C:\velapanel.com\data\panel.db";

SQLitePCL.Batteries_V2.Init();

var opts = ParseArgs(args, out string command);

string cs = new SqliteConnectionStringBuilder { DataSource = DbPath, Mode = SqliteOpenMode.ReadWrite }.ToString();
using var conn = new SqliteConnection(cs);
conn.Open();

switch (command)
{
    case "list":
        ListUsers(conn, opts.GetValueOrDefault("role"));
        break;
    case "set":
        SetUser(conn, opts);
        break;
    default:
        PrintUsage();
        break;
}

// ---------------------------------------------------------------------------

void ListUsers(SqliteConnection c, string roleFilter)
{
    var sql = @"
        SELECT u.Id, u.UserName, u.Email, u.FullName, u.IsActive,
               u.EmailConfirmed, u.LockoutEnd, u.AccessFailedCount,
               IFNULL(GROUP_CONCAT(r.Name, ','), '') AS Roles
        FROM AspNetUsers u
        LEFT JOIN AspNetUserRoles ur ON ur.UserId = u.Id
        LEFT JOIN AspNetRoles     r  ON r.Id = ur.RoleId
        GROUP BY u.Id
        ORDER BY u.UserName;";

    using var cmd = c.CreateCommand();
    cmd.CommandText = sql;
    using var rd = cmd.ExecuteReader();

    Console.WriteLine();
    Console.WriteLine("VelaPanel accounts");
    Console.WriteLine(new string('-', 100));
    int n = 0;
    while (rd.Read())
    {
        var roles = rd["Roles"]?.ToString() ?? "";
        if (!string.IsNullOrEmpty(roleFilter) &&
            !roles.Split(',').Any(x => x.Equals(roleFilter, StringComparison.OrdinalIgnoreCase)))
            continue;

        n++;
        Console.WriteLine($"UserName : {rd["UserName"]}");
        Console.WriteLine($"Email    : {rd["Email"]}");
        Console.WriteLine($"FullName : {rd["FullName"]}");
        Console.WriteLine($"Roles    : {roles}");
        Console.WriteLine($"Id       : {rd["Id"]}");
        Console.WriteLine($"Active   : {rd["IsActive"]}   EmailConfirmed: {rd["EmailConfirmed"]}   " +
                          $"LockoutEnd: {(rd["LockoutEnd"] == DBNull.Value ? "-" : rd["LockoutEnd"])}   " +
                          $"FailedLogins: {rd["AccessFailedCount"]}");
        Console.WriteLine(new string('-', 100));
    }
    if (n == 0) Console.WriteLine("(no matching users)");
    Console.WriteLine();
    Console.WriteLine("Note: passwords are stored one-way hashed and CANNOT be retrieved — only reset with 'set'.");
}

void SetUser(SqliteConnection c, Dictionary<string, string> o)
{
    if (!o.TryGetValue("user", out var selector) || string.IsNullOrWhiteSpace(selector))
    {
        Console.Error.WriteLine("ERROR: --user <Id | UserName | Email> is required for 'set'.");
        Environment.Exit(2);
        return;
    }

    // Locate the target user by Id, UserName or Email (case-insensitive).
    string id, curUser, curEmail;
    using (var find = c.CreateCommand())
    {
        find.CommandText = @"
            SELECT Id, UserName, Email FROM AspNetUsers
            WHERE Id = $s
               OR UserName = $s COLLATE NOCASE
               OR Email    = $s COLLATE NOCASE
            LIMIT 2;";
        find.Parameters.AddWithValue("$s", selector);
        using var rd = find.ExecuteReader();
        if (!rd.Read())
        {
            Console.Error.WriteLine($"ERROR: no user matched '{selector}'. Run 'list' to see accounts.");
            Environment.Exit(3);
            return;
        }
        id = rd["Id"].ToString();
        curUser = rd["UserName"].ToString();
        curEmail = rd["Email"].ToString();
        if (rd.Read())
        {
            Console.Error.WriteLine($"ERROR: '{selector}' matched more than one user. Use the exact Id.");
            Environment.Exit(4);
            return;
        }
    }

    var sets = new List<string>();
    using var upd = c.CreateCommand();

    // Username change (updates UserName + normalized form)
    if (o.TryGetValue("username", out var newUser) && !string.IsNullOrWhiteSpace(newUser))
    {
        sets.Add("UserName = $un");
        sets.Add("NormalizedUserName = $nun");
        upd.Parameters.AddWithValue("$un", newUser);
        upd.Parameters.AddWithValue("$nun", newUser.ToUpperInvariant());
    }

    // Email change (updates Email + normalized form)
    if (o.TryGetValue("email", out var newEmail) && !string.IsNullOrWhiteSpace(newEmail))
    {
        sets.Add("Email = $em");
        sets.Add("NormalizedEmail = $nem");
        sets.Add("EmailConfirmed = 1");
        upd.Parameters.AddWithValue("$em", newEmail);
        upd.Parameters.AddWithValue("$nem", newEmail.ToUpperInvariant());
    }

    // Password change
    if (o.TryGetValue("password", out var newPass) && !string.IsNullOrWhiteSpace(newPass))
    {
        sets.Add("PasswordHash = $ph");
        upd.Parameters.AddWithValue("$ph", HashPasswordV3(newPass));
    }

    if (sets.Count == 0)
    {
        Console.Error.WriteLine("ERROR: nothing to change. Provide at least one of --username, --email, --password.");
        Environment.Exit(5);
        return;
    }

    // Always: unlock, clear failed logins, activate, and rotate stamps so old cookies die.
    sets.Add("LockoutEnd = NULL");
    sets.Add("AccessFailedCount = 0");
    sets.Add("IsActive = 1");
    sets.Add("SecurityStamp = $ss");
    sets.Add("ConcurrencyStamp = $cs");
    upd.Parameters.AddWithValue("$ss", Guid.NewGuid().ToString());
    upd.Parameters.AddWithValue("$cs", Guid.NewGuid().ToString());

    upd.CommandText = $"UPDATE AspNetUsers SET {string.Join(", ", sets)} WHERE Id = $id;";
    upd.Parameters.AddWithValue("$id", id);
    int rows = upd.ExecuteNonQuery();

    Console.WriteLine();
    Console.WriteLine(rows == 1 ? "SUCCESS — account updated." : $"WARNING — {rows} rows affected.");
    Console.WriteLine($"  Id            : {id}");
    Console.WriteLine($"  UserName      : {(o.ContainsKey("username") ? o["username"] + "  (was " + curUser + ")" : curUser)}");
    Console.WriteLine($"  Email         : {(o.ContainsKey("email") ? o["email"] + "  (was " + curEmail + ")" : curEmail)}");
    if (o.ContainsKey("password")) Console.WriteLine("  Password      : reset");
    Console.WriteLine("  Lockout       : cleared, account activated, existing sessions invalidated.");
    Console.WriteLine();
    Console.WriteLine("Log in with the UserName (or Email) above. Change the password again from My Profile after login.");
}

// ASP.NET Core Identity PasswordHasher v3 format (0x01 marker, HMACSHA256).
string HashPasswordV3(string password)
{
    const int prf = 1;            // KeyDerivationPrf.HMACSHA256
    const int iter = 100_000;
    const int saltSize = 16;
    const int subkeySize = 32;

    byte[] salt = RandomNumberGenerator.GetBytes(saltSize);
    byte[] subkey = Rfc2898DeriveBytes.Pbkdf2(
        System.Text.Encoding.UTF8.GetBytes(password),
        salt, iter, HashAlgorithmName.SHA256, subkeySize);

    byte[] outBytes = new byte[13 + salt.Length + subkey.Length];
    outBytes[0] = 0x01;
    WriteUInt32BE(outBytes, 1, prf);
    WriteUInt32BE(outBytes, 5, iter);
    WriteUInt32BE(outBytes, 9, (uint)salt.Length);
    Buffer.BlockCopy(salt, 0, outBytes, 13, salt.Length);
    Buffer.BlockCopy(subkey, 0, outBytes, 13 + salt.Length, subkey.Length);
    return Convert.ToBase64String(outBytes);
}

void WriteUInt32BE(byte[] buf, int offset, uint value)
{
    buf[offset + 0] = (byte)(value >> 24);
    buf[offset + 1] = (byte)(value >> 16);
    buf[offset + 2] = (byte)(value >> 8);
    buf[offset + 3] = (byte)(value);
}

Dictionary<string, string> ParseArgs(string[] a, out string cmd)
{
    cmd = a.Length > 0 && !a[0].StartsWith("--") ? a[0].ToLowerInvariant() : "list";
    var d = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
    for (int i = 0; i < a.Length; i++)
    {
        if (a[i].StartsWith("--"))
        {
            var key = a[i].Substring(2);
            var val = (i + 1 < a.Length && !a[i + 1].StartsWith("--")) ? a[++i] : "true";
            d[key] = val;
        }
    }
    return d;
}

void PrintUsage()
{
    Console.WriteLine(@"
VelaPanel admin recovery tool

  list  [--role Admin]                       Show accounts (username, email, roles). Password cannot be shown.
  set   --user <Id|UserName|Email> [options] Reset credentials for one account:
          --username <newUserName>
          --email    <newEmail>
          --password <newPassword>           (min 12 chars, upper+lower+digit+symbol)

Examples:
  AdminReset list --role Admin
  AdminReset set --user admin@localhost --password ""NewAdmin@Pass1!""
  AdminReset set --user admin@localhost --username newadmin@site.com --email newadmin@site.com --password ""NewAdmin@Pass1!""
");
}
