using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using Amazon;
using Amazon.Runtime;
using Amazon.S3;
using BencodeNET.Objects;
using BencodeNET.Torrents;
using VoidCat.Database;
using VoidCat.Model.Exceptions;
using File = VoidCat.Database.File;

namespace VoidCat.Model;

public static class Extensions
{
    public static AmazonS3Client CreateClient(this S3BlobConfig c)
    {
        AWSConfigsS3.UseSignatureVersion4 = true;
        return new AmazonS3Client(new BasicAWSCredentials(c.AccessKey, c.SecretKey),
            new AmazonS3Config
            {
                RegionEndpoint = !string.IsNullOrEmpty(c.Region) ? RegionEndpoint.GetBySystemName(c.Region) : null,
                ServiceURL = c.ServiceUrl?.ToString(),
                UseHttp = c.ServiceUrl?.Scheme == "http",
                ForcePathStyle = true
            });
    }

    public static Guid? GetUserId(this HttpContext context)
    {
        var claimSub = context.User.Claims.FirstOrDefault(a => a.Type == ClaimTypes.NameIdentifier)?.Value;
        return Guid.TryParse(claimSub, out var g) ? g : null;
    }

    public static string? GetPubKey(this HttpContext context)
    {
        var claim = context.User.Claims.FirstOrDefault(a => a.Type == ClaimTypes.Name);
        return claim?.Value;
    }
    
    public static IEnumerable<string>? GetUserRoles(this HttpContext context)
    {
        return context?.User?.Claims?.Where(a => a.Type == ClaimTypes.Role)
            ?.Select(a => a?.Value!);
    }

    public static bool IsRole(this HttpContext context, string role)
    {
        return GetUserRoles(context)?.Contains(role) ?? false;
    }

    public static Guid FromBase58Guid(this string base58)
    {
        var enc = new NBitcoin.DataEncoders.Base58Encoder();
        base58 = Path.GetFileNameWithoutExtension(base58);
        var guidBytes = enc.DecodeData(base58);
        if (guidBytes.Length != 16) throw new VoidInvalidIdException(base58);

        return new Guid(guidBytes);
    }

    public static bool TryFromBase58Guid(this string base58, out Guid v)
    {
        try
        {
            v = base58.FromBase58Guid();
            return true;
        }
        catch
        {
            // ignored
            v = Guid.Empty;
            return false;
        }
    }

    public static string ToBase58(this Guid id)
    {
        var enc = new NBitcoin.DataEncoders.Base58Encoder();
        return enc.EncodeData(id.ToByteArray());
    }

    public static string? GetHeader(this IHeaderDictionary headers, string key)
    {
        var h = headers
            .FirstOrDefault(a => a.Key.Equals(key, StringComparison.InvariantCultureIgnoreCase));

        return !string.IsNullOrEmpty(h.Value.ToString()) ? h.Value.ToString() : default;
    }

    public static bool CanEdit(this File file, Guid? editSecret)
    {
        return file.EditSecret == editSecret;
    }

    public static string ToHex(this byte[] data)
    {
        return BitConverter.ToString(data).Replace("-", string.Empty).ToLower();
    }

    private static int HexToInt(char c)
    {
        switch (c)
        {
            case '0':
                return 0;
            case '1':
                return 1;
            case '2':
                return 2;
            case '3':
                return 3;
            case '4':
                return 4;
            case '5':
                return 5;
            case '6':
                return 6;
            case '7':
                return 7;
            case '8':
                return 8;
            case '9':
                return 9;
            case 'a':
            case 'A':
                return 10;
            case 'b':
            case 'B':
                return 11;
            case 'c':
            case 'C':
                return 12;
            case 'd':
            case 'D':
                return 13;
            case 'e':
            case 'E':
                return 14;
            case 'f':
            case 'F':
                return 15;
            default:
                throw new FormatException("Unrecognized hex char " + c);
        }
    }

    private static readonly byte[,] ByteLookup = new byte[,]
    {
        // low nibble
        {
            0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
            0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f
        },
        // high nibble
        {
            0x00, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70,
            0x80, 0x90, 0xa0, 0xb0, 0xc0, 0xd0, 0xe0, 0xf0
        }
    };

    public static byte[] FromHex(this string input)
    {
        var result = new byte[(input.Length + 1) >> 1];
        var lastCell = result.Length - 1;
        var lastChar = input.Length - 1;
        for (var i = 0; i < input.Length; i++)
        {
            result[lastCell - (i >> 1)] |= ByteLookup[i & 1, HexToInt(input[lastChar - i])];
        }

        return result;
    }

    public static string HashPassword(this string password)
    {
        return password.Hash("pbkdf2");
    }

    public static string Hash(this string password, string algo, string? saltHex = null)
    {
        var bytes = Encoding.UTF8.GetBytes(password);
        return Hash(bytes, algo, saltHex);
    }

    public static string Hash(this byte[] bytes, string algo, string? saltHex = null)
    {
        switch (algo)
        {
            case "md5":
            {
                var hash = MD5.Create().ComputeHash(bytes);
                return $"md5:{hash.ToHex()}";
            }
            case "sha1":
            {
                var hash = SHA1.Create().ComputeHash(bytes);
                return $"sha1:{hash.ToHex()}";
            }
            case "sha256":
            {
                var hash = SHA256.Create().ComputeHash(bytes);
                return $"sha256:{hash.ToHex()}";
            }
            case "sha512":
            {
                var hash = SHA512.Create().ComputeHash(bytes);
                return $"sha512:{hash.ToHex()}";
            }
            case "pbkdf2":
            {
                const int saltSize = 32;
                const int iterations = 310_000;

                var salt = new byte[saltSize];
                if (saltHex == default)
                {
                    RandomNumberGenerator.Fill(salt);
                }
                else
                {
                    salt = saltHex.FromHex();
                }

                var pbkdf2 = new Rfc2898DeriveBytes(bytes, salt, iterations);
                return $"pbkdf2:{salt.ToHex()}:{pbkdf2.GetBytes(salt.Length).ToHex()}";
            }
        }

        throw new ArgumentException("Unknown algo", nameof(algo));
    }

    /// <summary>
    /// Validate password matches hashed password
    /// </summary>
    /// <param name="vu"></param>
    /// <param name="password"></param>
    /// <returns></returns>
    /// <exception cref="InvalidOperationException"></exception>
    public static bool CheckPassword(this Database.User vu, string password)
    {
        if (vu.AuthType != UserAuthType.Internal)
            throw new InvalidOperationException("User type is not internal, cannot check password!");

        if (string.IsNullOrEmpty(vu.Password))
            throw new InvalidOperationException("User password is not set");

        var hashParts = vu.Password!.Split(":");
        return vu.Password == password.Hash(hashParts[0], hashParts.Length == 3 ? hashParts[1] : null);
    }

    /// <summary>
    /// Patch metadata
    /// </summary>
    /// <param name="oldMeta"></param>
    /// <param name="meta"></param>
    public static void Patch(this File oldMeta, File meta)
    {
        oldMeta.Description = meta.Description ?? oldMeta.Description;
        oldMeta.Name = meta.Name ?? oldMeta.Name;
        oldMeta.MimeType = meta.MimeType ?? oldMeta.MimeType;
        oldMeta.Storage = meta.Storage ?? oldMeta.Storage;
        oldMeta.Expires = meta.Expires;
        oldMeta.EncryptionParams = meta.EncryptionParams ?? oldMeta.EncryptionParams;
    }

    public static bool HasPostgres(this VoidSettings settings)
        => !string.IsNullOrEmpty(settings.Postgres);

    public static bool HasRedis(this VoidSettings settings)
        => !string.IsNullOrEmpty(settings.Redis);

    public static bool HasPrometheus(this VoidSettings settings)
        => settings.Prometheus?.Url != null;

    public static bool HasVirusScanner(this VoidSettings settings)
        => settings.VirusScanner?.ClamAV != default || settings.VirusScanner?.VirusTotal != default;

    public static bool HasPlausible(this VoidSettings settings)
        => settings.PlausibleAnalytics?.Endpoint != null;

    public static bool HasDiscord(this VoidSettings settings)
        => settings.Discord != null;

    public static bool HasGoogle(this VoidSettings settings)
        => settings.Google != null;

    public static async Task<Torrent> MakeTorrent(this VoidFileMeta meta, Guid id, Stream fileStream, Uri baseAddress,
        List<string> trackers)
    {
        const int pieceSize = 262_144;
        const int pieceHashLen = 20;
        var webSeed = new UriBuilder(baseAddress)
        {
            Path = $"/d/{id.ToBase58()}"
        };

        async Task<byte[]> BuildPieces()
        {
            fileStream.Seek(0, SeekOrigin.Begin);
            var chunks = (int)Math.Ceiling(meta.Size / (decimal)pieceSize);
            var hashes = new byte[pieceHashLen * chunks];
            var chunk = new byte[pieceSize];
            for (var x = 0; x < chunks; x++)
            {
                var rLen = await fileStream.ReadAsync(chunk, 0, chunk.Length);
                var hash = SHA1.HashData(chunk.AsSpan(0, rLen));
                Buffer.BlockCopy(hash, 0, hashes, x * pieceHashLen, pieceHashLen);
            }

            return hashes;
        }

        // build magnet link
        var t = new Torrent()
        {
            File = new()
            {
                FileName = meta.Name,
                FileSize = (long)meta.Size
            },
            Comment = meta.Name,
            CreationDate = meta.Uploaded,
            IsPrivate = false,
            PieceSize = pieceSize,
            Pieces = await BuildPieces(),
            // ReSharper disable once CoVariantArrayConversion
            Trackers = trackers.Select(a => new[] {a}).ToArray(),
            ExtraFields = new BDictionary
            {
                {"url-list", webSeed.ToString()}
            }
        };

        return t;
    }

    public static VoidFileResponse ToResponse(this File f, bool withEditSecret)
    {
        return new()
        {
            Id = f.Id,
            Metadata = f.ToMeta(withEditSecret)
        };
    }

    public static VoidFileMeta ToMeta(this File f, bool withEditSecret)
    {
        return new()
        {
            Name = f.Name,
            Description = f.Description,
            Uploaded = f.Uploaded,
            MimeType = f.MimeType,
            Size = f.Size,
            Digest = f.Digest,
            Expires = f.Expires,
            EditSecret = withEditSecret ? f.EditSecret : null,
            Storage = f.Storage,
            EncryptionParams = f.EncryptionParams,
            MagnetLink = f.MagnetLink
        };
    }

    public static ApiUser ToApiUser(this User u, bool isSelf)
    {
        return new()
        {
            Id = u.Id,
            Name = u.DisplayName,
            Avatar = u.Avatar,
            Created = u.Created,
            NeedsVerification = isSelf ? !u.Flags.HasFlag(UserFlags.EmailVerified) : null,
            PublicProfile = u.Flags.HasFlag(UserFlags.PublicProfile),
            PublicUploads = u.Flags.HasFlag(UserFlags.PublicUploads),
            Roles = u.Roles.Select(a => a.Role).ToList()
        };
    }

    public static AdminApiUser ToAdminApiUser(this User u, bool isSelf)
    {
        return new()
        {
            Id = u.Id,
            Name = u.DisplayName,
            Avatar = u.Avatar,
            Created = u.Created,
            NeedsVerification = isSelf ? !u.Flags.HasFlag(UserFlags.EmailVerified) : null,
            PublicProfile = u.Flags.HasFlag(UserFlags.PublicProfile),
            PublicUploads = u.Flags.HasFlag(UserFlags.PublicUploads),
            Roles = u.Roles.Select(a => a.Role).ToList(),
            Storage = u.Storage,
            Email = u.Email
        };
    }
    public static VirusStatus ToVirusStatus(this VirusScanResult r)
    {
        return new()
        {
            ScanTime = r.ScanTime,
            IsVirus = r.Score > 0.7m,
            Names = r.Names
        };
    }
}
