using System.Text.RegularExpressions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Playwright;
using Newtonsoft.Json;
using OF_DL.Models;
using Serilog;
using UserEntities = OF_DL.Models.Entities.Users;

namespace OF_DL.Services;

public class AuthService(IServiceProvider serviceProvider) : IAuthService
{
    private const float LoginTimeout = 600000f; // 10 minutes
    private const float FeedLoadTimeout = 60000f; // 1 minute
    private const int AdditionalWaitAfterPageLoad = 3000; // 3 seconds

    private readonly string _userDataDir = Path.GetFullPath("chromium-data");
    private const string InitScriptsDirName = "chromium-scripts";

    private readonly BrowserTypeLaunchPersistentContextOptions _options = new()
    {
        Headless = false,
        Channel = "chromium",
        Args =
        [
            "--no-sandbox",
            "--disable-setuid-sandbox",
            "--disable-blink-features=AutomationControlled",
            "--disable-infobars"
        ]
    };

    private readonly string[] _desiredCookies =
    [
        "auth_id",
        "sess"
    ];


    /// <summary>
    /// Gets or sets the current authentication state.
    /// </summary>
    public Auth? CurrentAuth { get; set; }

    /// <summary>
    /// Loads authentication data from the disk.
    /// </summary>
    /// <param name="filePath">The auth file path.</param>
    /// <returns>True when auth data is loaded successfully.</returns>
    public async Task<bool> LoadFromFileAsync(string filePath = "auth.json")
    {
        try
        {
            if (!File.Exists(filePath))
            {
                Log.Debug("Auth file not found: {FilePath}", filePath);
                return false;
            }

            string json = await File.ReadAllTextAsync(filePath);
            CurrentAuth = JsonConvert.DeserializeObject<Auth>(json);
            Log.Debug("Auth file loaded and deserialized successfully");
            return CurrentAuth != null;
        }
        catch (Exception ex)
        {
            Log.Error(ex, "Failed to load auth from file");
            return false;
        }
    }

    /// <summary>
    /// Launches a browser session and extracts auth data after login.
    /// </summary>
    /// <returns>True when auth data is captured successfully.</returns>
    public async Task<bool> LoadFromBrowserAsync()
    {
        try
        {
            bool runningInDocker = Environment.GetEnvironmentVariable("OFDL_DOCKER") != null;

            await SetupBrowser(runningInDocker);
            CurrentAuth = await GetAuthFromBrowser();

            return CurrentAuth != null;
        }
        catch (Exception ex)
        {
            Log.Error(ex, "Failed to load auth from browser");
            return false;
        }
    }

    /// <summary>
    /// Persists the current auth data to disk.
    /// </summary>
    /// <param name="filePath">The auth file path.</param>
    public async Task SaveToFileAsync(string filePath = "auth.json")
    {
        if (CurrentAuth == null)
        {
            Log.Warning("Attempted to save null auth to file");
            return;
        }

        try
        {
            string json = JsonConvert.SerializeObject(CurrentAuth, Formatting.Indented);
            await File.WriteAllTextAsync(filePath, json);
            Log.Debug($"Auth saved to file: {filePath}");
        }
        catch (Exception ex)
        {
            Log.Error(ex, "Failed to save auth to file");
        }
    }

    private Task SetupBrowser(bool runningInDocker)
    {
        if (runningInDocker)
        {
            Log.Information("Running in Docker. Disabling sandbox and GPU.");
            _options.Args =
            [
                "--no-sandbox", "--disable-setuid-sandbox", "--disable-gpu",
                "--disable-blink-features=AutomationControlled", "--disable-infobars"
            ];

            // If chromium is already downloaded, skip installation
            string? playwrightBrowsersPath = Environment.GetEnvironmentVariable("PLAYWRIGHT_BROWSERS_PATH");
            IEnumerable<string> folders = Directory.GetDirectories(playwrightBrowsersPath ?? "/config/chromium")
                .Where(folder => folder.Contains("chromium-"));

            if (folders.Any())
            {
                Log.Information("chromium already downloaded. Skipping install step.");
                return Task.CompletedTask;
            }
        }

        int exitCode = Program.Main(["install", "--with-deps", "chromium"]);
        return exitCode != 0
            ? throw new Exception($"Playwright chromium download failed. Exited with code {exitCode}")
            : Task.CompletedTask;
    }

    private static async Task<string> GetBcToken(IPage page) =>
        await page.EvaluateAsync<string>("window.localStorage.getItem('bcTokenSha') || ''");

    /// <summary>
    /// Normalizes the stored cookie string to only include required cookie values.
    /// </summary>
    public void ValidateCookieString()
    {
        if (CurrentAuth == null)
        {
            return;
        }

        if (string.IsNullOrWhiteSpace(CurrentAuth.Cookie))
        {
            return;
        }

        string pattern = @"(auth_id=\d+)|(sess=[^;]+)";
        MatchCollection matches = Regex.Matches(CurrentAuth.Cookie, pattern);

        string output = string.Join("; ", matches);

        if (!output.EndsWith(";"))
        {
            output += ";";
        }

        if (CurrentAuth.Cookie.Trim() != output.Trim())
        {
            CurrentAuth.Cookie = output;
            string newAuthString = JsonConvert.SerializeObject(CurrentAuth, Formatting.Indented);
            File.WriteAllText("auth.json", newAuthString);
        }
    }

    /// <summary>
    /// Validates auth by requesting the current user profile.
    /// </summary>
    /// <returns>The authenticated user or null when validation fails.</returns>
    public async Task<UserEntities.User?> ValidateAuthAsync()
    {
        // Resolve IApiService lazily to avoid circular dependency
        IApiService apiService = serviceProvider.GetRequiredService<IApiService>();
        return await apiService.GetUserInfo("/users/me");
    }

    /// <summary>
    /// Clears persisted auth data and browser profile state.
    /// </summary>
    public void Logout()
    {
        if (Directory.Exists("chromium-data"))
        {
            Log.Information("Deleting chromium-data folder");
            Directory.Delete("chromium-data", true);
        }

        if (File.Exists("auth.json"))
        {
            Log.Information("Deleting auth.json");
            File.Delete("auth.json");
        }
    }

    private async Task<Auth?> GetAuthFromBrowser()
    {
        try
        {
            IBrowserContext? browser;
            try
            {
                IPlaywright playwright = await Playwright.CreateAsync();
                browser = await playwright.Chromium.LaunchPersistentContextAsync(_userDataDir, _options);
            }
            catch (Exception e)
            {
                if ((
                        e.Message.Contains("An error occurred trying to start process") ||
                        e.Message.Contains("The profile appears to be in use by another Chromium process")
                    ) && Directory.Exists(_userDataDir))
                {
                    Log.Error("Failed to launch browser. Deleting chromium-data directory and trying again.");
                    Directory.Delete(_userDataDir, true);
                    IPlaywright playwright = await Playwright.CreateAsync();
                    browser = await playwright.Chromium.LaunchPersistentContextAsync(_userDataDir, _options);
                }
                else
                {
                    throw;
                }
            }

            if (browser == null)
            {
                throw new Exception("Could not get browser");
            }

            IPage? page = browser.Pages[0];

            if (page == null)
            {
                throw new Exception("Could not get page");
            }

            string exeDirectory = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location) ??
                                  string.Empty;
            string initScriptsDir = Path.Combine(exeDirectory, InitScriptsDirName);
            if (Directory.Exists(initScriptsDir))
            {
                Log.Information("Loading init scripts from {initScriptsDir}", initScriptsDir);
                foreach (string initScript in Directory.GetFiles(initScriptsDir, "*.js"))
                {
                    Log.Debug("Loading init script {initScript}", initScript);
                    await page.AddInitScriptAsync(initScript);
                }
            }

            Log.Debug("Navigating to OnlyFans.");
            await page.GotoAsync("https://onlyfans.com");

            Log.Debug("Waiting for user to login");
            await page.WaitForSelectorAsync(".b-feed", new PageWaitForSelectorOptions { Timeout = LoginTimeout });
            Log.Debug("Feed element detected (user logged in)");

            await page.ReloadAsync(
                new PageReloadOptions { Timeout = FeedLoadTimeout, WaitUntil = WaitUntilState.DOMContentLoaded });

            // Wait for an additional time to ensure the DOM is fully loaded
            await Task.Delay(AdditionalWaitAfterPageLoad);

            Log.Debug("DOM loaded. Getting BC token and cookies ...");

            string xBc;
            try
            {
                xBc = await GetBcToken(page);
            }
            catch (Exception e)
            {
                await browser.CloseAsync();
                throw new Exception($"Error getting bcToken. {e.Message}");
            }

            Dictionary<string, string> mappedCookies = (await browser.CookiesAsync())
                .Where(cookie => cookie.Domain.Contains("onlyfans.com"))
                .ToDictionary(cookie => cookie.Name, cookie => cookie.Value);

            mappedCookies.TryGetValue("auth_id", out string? userId);
            if (userId == null)
            {
                await browser.CloseAsync();
                throw new Exception("Could not find 'auth_id' cookie");
            }

            mappedCookies.TryGetValue("sess", out string? sess);
            if (sess == null)
            {
                await browser.CloseAsync();
                throw new Exception("Could not find 'sess' cookie");
            }

            string userAgent = await page.EvaluateAsync<string>("navigator.userAgent");
            if (string.IsNullOrWhiteSpace(userAgent))
            {
                await browser.CloseAsync();
                throw new Exception("Could not get user agent");
            }

            string cookies = string.Join(" ", mappedCookies.Keys.Where(key => _desiredCookies.Contains(key))
                .Select(key => $"${key}={mappedCookies[key]};"));

            await browser.CloseAsync();

            return new Auth { Cookie = cookies, UserAgent = userAgent, UserId = userId, XBc = xBc };
        }
        catch (Exception e)
        {
            Log.Error(e, "Error getting auth from browser");
            return null;
        }
    }
}
