using System.Text;
using System.Text.Json;
using ERPCore.Infra.Gl;
using ERPCore.Services.Gl;
using ERPCore.Services.Interfaces;
using ERPCore.System.Errors;

namespace ERPCore.Services;

/// <inheritdoc cref="IGeneralLedgerService"/>
public sealed class GeneralLedgerService : IGeneralLedgerService
{
    private static readonly JsonSerializerOptions JsonOptions = new()
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
        PropertyNameCaseInsensitive = true
    };

    private readonly IGeneralLedgerClient _client;

    public GeneralLedgerService(IGeneralLedgerClient client) => _client = client;

    public Task<GeneralLedgerResponse> ForwardAsync(
        HttpMethod method, string path, string? queryString, string? contentType, Stream? body, CancellationToken ct)
        => _client.SendAsync(method, path, queryString, contentType, body, ct);

    public async Task<GlJournalEntryResult> PostJournalEntryAsync(GlJournalEntryRequest request, CancellationToken ct)
    {
        using var body = new MemoryStream(JsonSerializer.SerializeToUtf8Bytes(request, JsonOptions));
        var response = await _client.SendAsync(HttpMethod.Post, "journal-entries", null, "application/json", body, ct);
        var data = ParseSuccess<GlJournalEntryData>(response);
        return new GlJournalEntryResult(data.JournalNo, data.IsPosted);
    }

    public async Task<GlPeriod> GetPeriodByDateAsync(DateOnly date, CancellationToken ct)
    {
        var response = await _client.SendAsync(
            HttpMethod.Get, "fiscal-years/periods/by-date", $"?date={date:yyyy-MM-dd}", null, null, ct);
        var data = ParseSuccess<GlPeriodData>(response);
        return new GlPeriod(data.PeriodId, data.FiscalYearId);
    }

    public async Task<IReadOnlyList<GlBankAccount>> ListBankAccountsAsync(CancellationToken ct)
    {
        var response = await _client.SendAsync(HttpMethod.Get, "bank-accounts", "?accountType=Both", null, null, ct);
        var data = ParseSuccess<List<GlBankAccountData>>(response);
        return data.Select(a => new GlBankAccount(
            a.AccountType, a.AccountId, a.AccountName, a.BankName,
            a.CashAccountTypeName, a.AccountNumber, a.GlAccountId, a.GlAccountCode, a.CurrencyCode)).ToList();
    }

    /// <summary>
    /// Parses GL's <c>ApiResponse</c> envelope (case-insensitively, since GL's success bodies are
    /// camelCase and its error bodies are PascalCase — docs/12 §4) and throws
    /// <see cref="DomainException"/> if the call didn't succeed.
    /// </summary>
    private static T ParseSuccess<T>(GeneralLedgerResponse response)
    {
        GlEnvelope<T>? envelope;
        try
        {
            envelope = JsonSerializer.Deserialize<GlEnvelope<T>>(response.Body, JsonOptions);
        }
        catch (JsonException)
        {
            envelope = null;
        }

        if (response.StatusCode is < 200 or >= 300 || envelope is null || !envelope.Success || envelope.Data is null)
        {
            var message = envelope?.Message ?? "The General Ledger service rejected the request.";
            var status = response.StatusCode is >= 400 and < 500 ? response.StatusCode : 502;
            throw new DomainException(ErrorCodes.GlRequestFailed, message, status);
        }

        return envelope.Data;
    }

    private sealed class GlEnvelope<T>
    {
        public int StatusCode { get; set; }
        public bool Success { get; set; }
        public string? Message { get; set; }
        public T? Data { get; set; }
    }

    private sealed class GlJournalEntryData
    {
        public string JournalNo { get; set; } = string.Empty;
        public bool IsPosted { get; set; }
    }

    private sealed class GlPeriodData
    {
        public int PeriodId { get; set; }
        public int FiscalYearId { get; set; }
    }

    private sealed class GlBankAccountData
    {
        public string AccountType { get; set; } = string.Empty;
        public long AccountId { get; set; }
        public string AccountName { get; set; } = string.Empty;
        public string? BankName { get; set; }
        public string? CashAccountTypeName { get; set; }
        public string? AccountNumber { get; set; }
        public long GlAccountId { get; set; }
        public string GlAccountCode { get; set; } = string.Empty;
        public string CurrencyCode { get; set; } = string.Empty;
    }
}
