using System.Runtime.InteropServices;
using System.Security.Cryptography;
using ErsatzTV.Core;

namespace ErsatzTV.Services;

/// <summary>
///     Resolves the effective API key used by <see cref="Filters.ApiAuthorizationFilter" /> and
///     the read-gating policy. Resolved once at startup: the configured <c>Api:WriteKey</c> wins;
///     otherwise a previously-persisted key is loaded from the config volume; otherwise a fresh
///     256-bit key is generated and persisted. The key is never empty, so write authorization is
///     always fail-closed (issue #280).
/// </summary>
public interface IApiKeyProvider
{
    /// <summary>The effective API key. Never null or empty.</summary>
    string ApiKey { get; }

    /// <summary>When true (the default), read (GET/HEAD) requests under <c>/api</c> also require the key.</summary>
    bool RequireKeyForReads { get; }
}

public sealed class ApiKeyProvider : IApiKeyProvider
{
    public const string WriteKeyConfigurationKey = "Api:WriteKey";
    public const string RequireKeyForReadsConfigurationKey = "Api:RequireKeyForReads";

    public ApiKeyProvider(IConfiguration configuration, ILogger<ApiKeyProvider> logger)
    {
        // Defense-in-depth default: gate reads too. Operators behind an authenticating proxy who
        // want anonymous catalog reads can opt out with Api:RequireKeyForReads=false.
        RequireKeyForReads = configuration.GetValue(RequireKeyForReadsConfigurationKey, true);
        ApiKey = ResolveKey(configuration, FileSystemLayout.ApiKeyPath, logger);
    }

    public string ApiKey { get; }

    public bool RequireKeyForReads { get; }

    internal static string ResolveKey(
        IConfiguration configuration,
        string keyFilePath,
        ILogger logger,
        Func<string, string> readKeyFile = null)
    {
        readKeyFile ??= File.ReadAllText;

        string configured = configuration[WriteKeyConfigurationKey];
        if (!string.IsNullOrWhiteSpace(configured))
        {
            logger.LogInformation("Using API key from configuration ({ConfigurationKey})", WriteKeyConfigurationKey);
            return configured.Trim();
        }

        if (File.Exists(keyFilePath))
        {
            try
            {
                string existing = readKeyFile(keyFilePath).Trim();
                if (!string.IsNullOrWhiteSpace(existing))
                {
                    logger.LogInformation("Loaded persisted API key from {Path}", keyFilePath);
                    return existing;
                }

                logger.LogWarning("Persisted API key file {Path} was empty; generating a new key", keyFilePath);
            }
            catch (Exception ex) when (ex is FileNotFoundException or DirectoryNotFoundException)
            {
                // Raced with a delete between the existence check and the read; treat as absent.
                logger.LogWarning(
                    "Persisted API key file {Path} disappeared before it could be read; generating a new key",
                    keyFilePath);
            }
            catch (Exception ex)
            {
                // Present but unreadable (e.g. wrong permissions). Fail loud rather than silently
                // generate + overwrite it, which would invalidate every client's stored key.
                logger.LogError(
                    ex,
                    "API key file {Path} exists but could not be read; refusing to overwrite it. Fix its "
                    + "permissions or set {ConfigurationKey}.",
                    keyFilePath,
                    WriteKeyConfigurationKey);
                throw;
            }
        }

        string generated = GenerateKey();
        Persist(keyFilePath, generated, logger);
        return generated;
    }

    internal static string GenerateKey() =>
        // 256 bits of entropy, rendered as lowercase hex so it is trivial to copy/paste with no
        // URL-/header-unsafe characters.
        Convert.ToHexString(RandomNumberGenerator.GetBytes(32)).ToLowerInvariant();

    private static void Persist(string path, string key, ILogger logger)
    {
        try
        {
            string directory = Path.GetDirectoryName(path);
            if (!string.IsNullOrEmpty(directory))
            {
                Directory.CreateDirectory(directory);
            }

            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                File.WriteAllText(path, key);
            }
            else
            {
                // Create the file owner-only (0600) up front so the key is never briefly world-readable
                // between write and chmod. SetUnixFileMode afterwards re-asserts it if the file pre-existed
                // (UnixCreateMode only applies to a newly-created file, not a truncated one).
                const UnixFileMode ownerReadWrite = UnixFileMode.UserRead | UnixFileMode.UserWrite;
                var options = new FileStreamOptions
                {
                    Mode = FileMode.Create,
                    Access = FileAccess.Write,
                    UnixCreateMode = ownerReadWrite
                };

                using (var stream = new FileStream(path, options))
                using (var writer = new StreamWriter(stream))
                {
                    writer.Write(key);
                }

                File.SetUnixFileMode(path, ownerReadWrite);
            }

            logger.LogWarning(
                "Generated a new API key and saved it to {Path}. Send it via the '{HeaderName}' header " +
                "(ChicoryTV: Settings → API Key). The key is required for API requests.",
                path,
                Filters.ApiAuthorizationFilter.HeaderName);
        }
        catch (Exception ex)
        {
            logger.LogError(
                ex,
                "Failed to persist generated API key to {Path}; it will not survive a restart. " +
                "Set {ConfigurationKey} to a fixed value to avoid this.",
                path,
                WriteKeyConfigurationKey);
        }
    }
}
