namespace Sonex.Client.Helpers;

using Npgsql;
using Sonex.Client.Dialogs;
using Sonex.Data.Records;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Runtime.ExceptionServices;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;

public static class Logger
{
    private static readonly object Sync = new();
    private static readonly UTF8Encoding Utf8NoBom = new(false);
    private static readonly JsonSerializerOptions JsonOptions = new()
    {
        WriteIndented = true,
        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
    };

    private static string? _currentPage;
    private static string? _currentPageTitle;

    // Chroni przed zapętleniem, gdy wyjątek pojawi się podczas samego zapisu logu.
    [ThreadStatic]
    private static bool _isWriting;

    /// <summary>
    /// Logger nie wymaga ciezkiej inicjalizacji przy starcie.
    /// Plik JSON powstaje dopiero przy pierwszym faktycznie zapisanym bledzie.
    /// </summary>
    public static void Initialize()
    {
    }

    /// <summary>
    /// Zapisuje zwykly obsluzony wyjatek aplikacji i zwraca identyfikator wpisu.
    /// </summary>
    public static string Error(
        Exception exception,
        string source,
        string operation,
        IReadOnlyDictionary<string, string?>? context = null,
        string? message = null)
    {
        ArgumentNullException.ThrowIfNull(exception);
        return Write("Error", exception, source, operation, message, context, isUnhandled: false);
    }

    /// <summary>
    /// Zapisuje prosty komunikat bledu bez obiektu wyjatku.
    /// </summary>
    public static string Error(
        string message,
        string source,
        string operation,
        IReadOnlyDictionary<string, string?>? context = null)
    {
        return Write("Error", null, source, operation, message, context, isUnhandled: false);
    }

    /// <summary>
    /// Zapisuje blad krytyczny. Kazdy wpis trafia do osobnego pliku JSON.
    /// </summary>
    public static string Fatal(
        Exception exception,
        string source,
        string operation,
        bool isUnhandled = true,
        IReadOnlyDictionary<string, string?>? context = null,
        string? message = null)
    {
        ArgumentNullException.ThrowIfNull(exception);
        return Write("Fatal", exception, source, operation, message, context, isUnhandled);
    }

    /// <summary>
    /// Rozpoznaje oczekiwane bledy PostgreSQL, ktore w tej aplikacji sa traktowane jako komunikaty biznesowe.
    /// </summary>
    public static bool IsBusinessDatabaseException(Exception exception)
    {
        ArgumentNullException.ThrowIfNull(exception);

        if (exception is PostgresException)
            return true;

        return exception.InnerException != null && IsBusinessDatabaseException(exception.InnerException);
    }

    /// <summary>
    /// Buduje krotki komunikat dla uzytkownika z identyfikatorem bledu.
    /// </summary>
    public static string BuildUserMessage(string errorId)
    {
        return $"Wystapil nieoczekiwany blad aplikacji. Identyfikator bledu: {errorId}.";
    }

    /// <summary>
    /// Zwraca wspolna lokalizacje folderu logow, przeniesiona o jeden poziom wyzej niz katalog binarny aplikacji.
    /// </summary>
    public static string GetLogsDirectoryPath()
    {
        return Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "Logs"));
    }

    /// <summary>
    /// Zapamietuje aktualna strone nawigacji, aby byla automatycznie dopisywana do logow.
    /// </summary>
    public static void SetCurrentPage(string? pageName, string? pageTitle = null)
    {
        lock (Sync)
        {
            _currentPage = Clean(pageName);
            _currentPageTitle = Clean(pageTitle);
        }
    }

    /// <summary>
    /// Pozostawione dla zgodnosci z istniejacym kodem.
    /// Przy pojedynczych plikach JSON nie ma tu nic do oprozniania.
    /// </summary>
    public static void FlushAndClose()
    {
    }

    /// <summary>
    /// Globalny handler dla AppDomain.CurrentDomain.UnhandledException.
    /// </summary>
    public static void OnException(object? sender, System.UnhandledExceptionEventArgs e)
    {
        var exception = e.ExceptionObject as Exception
            ?? new InvalidOperationException(e.ExceptionObject?.ToString() ?? "Unknown unhandled exception.");

        Fatal(
            exception,
            source: sender?.GetType().Name ?? "AppDomain",
            operation: "AppDomain.CurrentDomain.UnhandledException");
    }

    /// <summary>
    /// Globalny handler dla AppDomain.CurrentDomain.FirstChanceException.
    /// Jesli bedzie wlaczony, kazdy taki wpis zapisze osobny plik JSON.
    /// </summary>
    public static void OnException(object? sender, FirstChanceExceptionEventArgs e)
    {
        if (e.Exception == null)
            return;

        Write(
            "Warning",
            e.Exception,
            source: sender?.GetType().Name ?? "AppDomain",
            operation: "AppDomain.CurrentDomain.FirstChanceException",
            message: null,
            context: null,
            isUnhandled: false);
    }

    /// <summary>
    /// Globalny handler dla wyjatkow z nieobserwowanych taskow.
    /// </summary>
    public static void OnException(object? sender, UnobservedTaskExceptionEventArgs e)
    {
        Error(
            e.Exception,
            source: sender?.GetType().Name ?? nameof(TaskScheduler),
            operation: "TaskScheduler.UnobservedTaskException");
        e.SetObserved();
    }

    /// <summary>
    /// Globalny handler dla wyjatkow WinUI zglaszanych przez Application.UnhandledException.
    /// </summary>
    public static void OnException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
    {
        if (e.Exception == null)
            return;

        Fatal(
            e.Exception,
            source: sender.GetType().Name,
            operation: "Application.UnhandledException",
            message: e.Message);
    }

    /// <summary>
    /// Wspolna sciezka zapisu wszystkich wpisow.
    /// Kazdy wpis jest osobnym plikiem JSON w folderze Logs.
    /// </summary>
    private static string Write(
        string level,
        Exception? exception,
        string source,
        string operation,
        string? message,
        IReadOnlyDictionary<string, string?>? context,
        bool isUnhandled)
    {
        string errorId = CreateErrorId();
        if (_isWriting)
            return errorId;

        _isWriting = true;

        try
        {
            DateTime occurredAt = DateTime.Now;
            AppLoggerRecord entry = BuildEntry(
                errorId,
                level,
                occurredAt,
                exception,
                source,
                operation,
                message,
                context,
                isUnhandled);

            string logDirectory = GetLogsDirectoryPath();
            Directory.CreateDirectory(logDirectory);

            string filePath = Path.Combine(
                logDirectory,
                $"{occurredAt:yyyyMMdd-HHmmssfff}-{level.ToLowerInvariant()}-{errorId}.json");

            string json = JsonSerializer.Serialize(entry, JsonOptions);
            File.WriteAllText(filePath, json, Utf8NoBom);
        }
        catch (Exception ex)
        {
            WriteFallback(ex);
        }
        finally
        {
            _isWriting = false;
        }

        return errorId;
    }

    /// <summary>
    /// Buduje obiekt, ktory potem zapisujemy bezposrednio do JSON.
    /// </summary>
    private static AppLoggerRecord BuildEntry(
        string errorId,
        string level,
        DateTime occurredAt,
        Exception? exception,
        string source,
        string operation,
        string? message,
        IReadOnlyDictionary<string, string?>? context,
        bool isUnhandled)
    {
        return new AppLoggerRecord
        {
            ErrorId = errorId,
            OccurredAt = AppLoggerRecord.FormatOccurredAt(occurredAt),
            Level = level,
            ApplicationName = AppSessionInfo.ApplicationName,
            AppVersion = AppInfo.InformationalVersion.Trim(),
            InstanceId = AppSessionInfo.InstanceId,
            UserName = GetUserName(),
            MachineName = Environment.MachineName,
            OsVersion = AppSessionInfo.OsVersion,
            CurrentPageTitle = GetCurrentPageTitle(),
            CurrentPage = GetCurrentPage(),
            Source = Clean(source) ?? source,
            Operation = Clean(operation) ?? operation,
            IsUnhandled = isUnhandled,
            Message = ResolveMessage(exception, message),
            ReportedMessage = GetReportedMessage(exception, message),
            ExceptionType = exception?.GetType().FullName,
            Exception = exception?.ToString(),
            Context = BuildContext(context),
            ExceptionData = BuildExceptionData(exception)
        };
    }

    private static string ResolveMessage(Exception? exception, string? message)
    {
        string? text = Clean(message);
        if (!string.IsNullOrWhiteSpace(text))
            return text;

        text = Clean(exception?.Message);
        if (!string.IsNullOrWhiteSpace(text))
            return text;

        return exception?.GetType().FullName ?? "Application error";
    }

    private static string? GetReportedMessage(Exception? exception, string? message)
    {
        string? cleanMessage = Clean(message);
        string? cleanExceptionMessage = Clean(exception?.Message);

        if (string.IsNullOrWhiteSpace(cleanMessage))
            return null;

        if (string.Equals(cleanMessage, cleanExceptionMessage, StringComparison.Ordinal))
            return null;

        return cleanMessage;
    }

    /// <summary>
    /// Zwraca dodatkowy kontekst przekazany z miejsca wywolania jako jeden plaski tekst.
    /// </summary>
    private static string? BuildContext(IReadOnlyDictionary<string, string?>? context)
    {
        if (context == null || context.Count == 0)
            return null;

        var builder = new StringBuilder();

        foreach (var pair in context)
        {
            string? cleanValue = Clean(pair.Value);
            if (string.IsNullOrWhiteSpace(cleanValue))
                continue;

            AppendPair(builder, pair.Key, cleanValue);
        }

        return builder.Length == 0 ? null : builder.ToString();
    }

    /// <summary>
    /// Przepisuje exception.Data do tekstu JSON.
    /// Dzięki temu glowny dokument pozostaje plaski, a dane dodatkowe dalej sa uporzadkowane.
    /// </summary>
    private static string? BuildExceptionData(Exception? exception)
    {
        if (exception?.Data == null || exception.Data.Count == 0)
            return null;

        var result = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
        int index = 0;

        foreach (DictionaryEntry item in exception.Data)
        {
            string key = Clean(item.Key?.ToString()) ?? $"Item{index}";
            string? value = Clean(item.Value?.ToString());

            if (string.IsNullOrWhiteSpace(value))
            {
                index++;
                continue;
            }

            result[key] = value;
            index++;
        }

        return result.Count == 0
            ? null
            : JsonSerializer.Serialize(result, JsonOptions);
    }

    private static string? GetCurrentPage()
    {
        lock (Sync)
        {
            return _currentPage;
        }
    }

    private static string? GetCurrentPageTitle()
    {
        lock (Sync)
        {
            return _currentPageTitle;
        }
    }

    private static string GetUserName()
    {
        string configured = Config.GetStringValue("USER_NAME", string.Empty);
        if (!string.IsNullOrWhiteSpace(configured))
            return configured;

        return Environment.UserName;
    }

    private static void AppendPair(StringBuilder builder, string key, string value)
    {
        if (builder.Length > 0)
            builder.Append(" | ");

        builder.Append(Clean(key) ?? key);
        builder.Append('=');
        builder.Append(value);
    }

    private static string? Clean(string? value)
    {
        if (string.IsNullOrWhiteSpace(value))
            return null;

        return value
            .Replace("\r", " ")
            .Replace("\n", " ")
            .Trim();
    }

    private static string CreateErrorId()
    {
        return Guid.NewGuid().ToString("N")[..12].ToUpperInvariant();
    }

    /// <summary>
    /// Awaryjny zapis do prostego pliku JSON, gdy standardowy zapis logu sam rzuci wyjatkiem.
    /// </summary>
    private static void WriteFallback(Exception exception)
    {
        try
        {
            DateTime occurredAt = DateTime.Now;
            string logDirectory = GetLogsDirectoryPath();
            Directory.CreateDirectory(logDirectory);

            string filePath = Path.Combine(
                logDirectory,
                $"{occurredAt:yyyyMMdd-HHmmssfff}-logger-fallback-{CreateErrorId()}.json");

            var fallback = new
            {
                OccurredAt = AppLoggerRecord.FormatOccurredAt(occurredAt),
                Message = "Logger internal failure.",
                Exception = exception.ToString()
            };

            string json = JsonSerializer.Serialize(fallback, JsonOptions);
            File.WriteAllText(filePath, json, Utf8NoBom);
        }
        catch
        {
        }
    }

}
