using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using Microsoft.Extensions.Logging;

namespace OTSSignsOrchestrator.Desktop.Services;

/// <summary>
/// Stores and retrieves operator JWT and refresh tokens using the OS credential store.
/// Windows: advapi32 Credential Manager; macOS: Security.framework Keychain;
/// Linux: AES-encrypted file fallback in AppData.
/// </summary>
public sealed class TokenStoreService
{
    private const string ServiceName = "OTSSignsOrchestrator";
    private const string JwtAccount = "operator-jwt";
    private const string RefreshAccount = "operator-refresh";

    private readonly ILogger<TokenStoreService> _logger;

    public TokenStoreService(ILogger<TokenStoreService> logger)
    {
        _logger = logger;
    }

    public void StoreTokens(string jwt, string refreshToken)
    {
        WriteCredential(JwtAccount, jwt);
        WriteCredential(RefreshAccount, refreshToken);
        _logger.LogDebug("Tokens stored in OS credential store");
    }

    public string? GetJwt() => ReadCredential(JwtAccount);

    public string? GetRefreshToken() => ReadCredential(RefreshAccount);

    public void ClearTokens()
    {
        DeleteCredential(JwtAccount);
        DeleteCredential(RefreshAccount);
        _logger.LogDebug("Tokens cleared from OS credential store");
    }

    // ── Platform dispatch ────────────────────────────────────────────────────

    private void WriteCredential(string account, string secret)
    {
        if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            WindowsCredentialManager.Write(ServiceName, account, secret);
        else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
            MacKeychain.Write(ServiceName, account, secret);
        else
            LinuxEncryptedFile.Write(ServiceName, account, secret);
    }

    private string? ReadCredential(string account)
    {
        if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            return WindowsCredentialManager.Read(ServiceName, account);
        if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
            return MacKeychain.Read(ServiceName, account);
        return LinuxEncryptedFile.Read(ServiceName, account);
    }

    private void DeleteCredential(string account)
    {
        if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            WindowsCredentialManager.Delete(ServiceName, account);
        else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
            MacKeychain.Delete(ServiceName, account);
        else
            LinuxEncryptedFile.Delete(ServiceName, account);
    }

    // ═══════════════════════════════════════════════════════════════════════
    // Windows — advapi32.dll Credential Manager
    // ═══════════════════════════════════════════════════════════════════════
    private static class WindowsCredentialManager
    {
        private const int CredTypeGeneric = 1;
        private const int CredPersistLocalMachine = 2;

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
        private struct CREDENTIAL
        {
            public uint Flags;
            public uint Type;
            public string TargetName;
            public string Comment;
            public long LastWritten;
            public uint CredentialBlobSize;
            public IntPtr CredentialBlob;
            public uint Persist;
            public uint AttributeCount;
            public IntPtr Attributes;
            public string TargetAlias;
            public string UserName;
        }

        [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
        private static extern bool CredWriteW(ref CREDENTIAL credential, uint flags);

        [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
        private static extern bool CredReadW(string target, uint type, uint flags, out IntPtr credential);

        [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
        private static extern bool CredDeleteW(string target, uint type, uint flags);

        [DllImport("advapi32.dll")]
        private static extern void CredFree(IntPtr buffer);

        private static string TargetName(string service, string account) => $"{service}/{account}";

        public static void Write(string service, string account, string secret)
        {
            var bytes = Encoding.UTF8.GetBytes(secret);
            var handle = GCHandle.Alloc(bytes, GCHandleType.Pinned);
            try
            {
                var cred = new CREDENTIAL
                {
                    Type = CredTypeGeneric,
                    TargetName = TargetName(service, account),
                    UserName = account,
                    CredentialBlob = handle.AddrOfPinnedObject(),
                    CredentialBlobSize = (uint)bytes.Length,
                    Persist = CredPersistLocalMachine,
                };
                CredWriteW(ref cred, 0);
            }
            finally { handle.Free(); }
        }

        public static string? Read(string service, string account)
        {
            if (!CredReadW(TargetName(service, account), CredTypeGeneric, 0, out var credPtr))
                return null;
            try
            {
                var cred = Marshal.PtrToStructure<CREDENTIAL>(credPtr);
                if (cred.CredentialBlobSize == 0 || cred.CredentialBlob == IntPtr.Zero) return null;
                var bytes = new byte[cred.CredentialBlobSize];
                Marshal.Copy(cred.CredentialBlob, bytes, 0, bytes.Length);
                return Encoding.UTF8.GetString(bytes);
            }
            finally { CredFree(credPtr); }
        }

        public static void Delete(string service, string account)
            => CredDeleteW(TargetName(service, account), CredTypeGeneric, 0);
    }

    // ═══════════════════════════════════════════════════════════════════════
    // macOS — Security.framework Keychain via /usr/bin/security CLI
    // ═══════════════════════════════════════════════════════════════════════
    private static class MacKeychain
    {
        public static void Write(string service, string account, string secret)
        {
            // Delete first to avoid "duplicate" errors on update
            Delete(service, account);
            RunSecurity($"add-generic-password -s \"{service}\" -a \"{account}\" -w \"{EscapeShell(secret)}\" -U");
        }

        public static string? Read(string service, string account)
        {
            var (exitCode, stdout) = RunSecurity($"find-generic-password -s \"{service}\" -a \"{account}\" -w");
            return exitCode == 0 ? stdout.Trim() : null;
        }

        public static void Delete(string service, string account)
            => RunSecurity($"delete-generic-password -s \"{service}\" -a \"{account}\"");

        private static (int exitCode, string stdout) RunSecurity(string args)
        {
            using var proc = new System.Diagnostics.Process();
            proc.StartInfo = new System.Diagnostics.ProcessStartInfo
            {
                FileName = "/usr/bin/security",
                Arguments = args,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                UseShellExecute = false,
                CreateNoWindow = true,
            };
            proc.Start();
            var stdout = proc.StandardOutput.ReadToEnd();
            proc.WaitForExit(5000);
            return (proc.ExitCode, stdout);
        }

        private static string EscapeShell(string s) => s.Replace("\\", "\\\\").Replace("\"", "\\\"");
    }

    // ═══════════════════════════════════════════════════════════════════════
    // Linux — AES-256-GCM encrypted file in ~/.local/share
    // ═══════════════════════════════════════════════════════════════════════
    private static class LinuxEncryptedFile
    {
        // Machine-specific key derived from machine-id + user name
        private static byte[] DeriveKey()
        {
            var machineId = "linux-default";
            try
            {
                if (File.Exists("/etc/machine-id"))
                    machineId = File.ReadAllText("/etc/machine-id").Trim();
            }
            catch { /* fallback */ }

            var material = $"{machineId}:{Environment.UserName}:{ServiceName}";
            return SHA256.HashData(Encoding.UTF8.GetBytes(material));
        }

        private static string FilePath(string service, string account)
        {
            var dir = Path.Combine(
                Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
                service, "credentials");
            Directory.CreateDirectory(dir);
            return Path.Combine(dir, $"{account}.enc");
        }

        public static void Write(string service, string account, string secret)
        {
            var key = DeriveKey();
            var plaintext = Encoding.UTF8.GetBytes(secret);
            var nonce = new byte[12];
            RandomNumberGenerator.Fill(nonce);
            var ciphertext = new byte[plaintext.Length];
            var tag = new byte[16];

            using var aes = new AesGcm(key, 16);
            aes.Encrypt(nonce, plaintext, ciphertext, tag);

            // File format: [12 nonce][16 tag][ciphertext]
            var output = new byte[12 + 16 + ciphertext.Length];
            nonce.CopyTo(output, 0);
            tag.CopyTo(output, 12);
            ciphertext.CopyTo(output, 28);
            File.WriteAllBytes(FilePath(service, account), output);
        }

        public static string? Read(string service, string account)
        {
            var path = FilePath(service, account);
            if (!File.Exists(path)) return null;

            var data = File.ReadAllBytes(path);
            if (data.Length < 28) return null;

            var nonce = data[..12];
            var tag = data[12..28];
            var ciphertext = data[28..];
            var plaintext = new byte[ciphertext.Length];

            using var aes = new AesGcm(DeriveKey(), 16);
            aes.Decrypt(nonce, ciphertext, tag, plaintext);
            return Encoding.UTF8.GetString(plaintext);
        }

        public static void Delete(string service, string account)
        {
            var path = FilePath(service, account);
            if (File.Exists(path)) File.Delete(path);
        }
    }
}
