using System.ComponentModel.DataAnnotations;
using System.Globalization;
using DoliMiddlewareApi.Dtos;
using DoliMiddlewareApi.Dtos.command;
using DoliMiddlewareApi.Dtos.Dolibarr;
using DoliMiddlewareApi.Dtos.query;
using DoliMiddlewareApi.Exceptions;
using DoliMiddlewareApi.Mappers;
using DoliMiddlewareApi.Services.Clients;
using DoliMiddlewareApi.Services.Notifications;

namespace DoliMiddlewareApi.Services;

public class InvoiceService(IDolibarrApiClient apiClient, INotificationService notifications)
{
    public async Task<InvoiceDetailDto> GetInvoiceAsync(int id)
    {
        var data = await apiClient.GetResourceAsync<InvoiceDetailResponse>($"invoices/{id}");
        var dto = InvoiceMapper.MapToInvoiceDetailDto(data);

        if (dto.ClientId > 0)
        {
            try
            {
                var client = await apiClient.GetResourceAsync<ClientResponse>($"thirdparties/{dto.ClientId}");
                dto.ClientName = client?.name;
            }
            catch { /* best-effort */ }
        }

        return dto;
    }

    public async Task<List<InvoiceDto>> GetInvoicesAsync(
        int limit = 50,
        int page = 1,
        string? status = null,
        string? search = null)
    {
        // empieza por 1 para el frontend
        var endpoint = $"invoices?limit={limit}&page={page - 1}";

        if (!string.IsNullOrEmpty(status))
        {
            endpoint += $"&status={status}";
        }

        if (!string.IsNullOrWhiteSpace(search))
        {
            search = search.Trim().ToUpperInvariant();
            var filter = $"(t.ref:like:'%{search}%')";
            endpoint += $"&sqlfilters={Uri.EscapeDataString(filter)}";
        }

        var dataList = await apiClient.GetCollectionAsync<InvoiceResponse>(endpoint);
        var invoices = dataList.Select(InvoiceMapper.MapToInvoiceDto).ToList();

        if (invoices.Count == 0)
            return invoices;

        var clientIds = invoices.Select(i => i.ClientId).Distinct().ToList();
        var clientNames = await GetClientNamesAsync(clientIds);

        foreach (var invoice in invoices)
        {
            invoice.ClientName = clientNames.GetValueOrDefault(invoice.ClientId);
        }

        return invoices;
    }

    public async Task<int> CreateInvoiceAsync(CreateInvoiceDto dto)
    {
        var requestBody = new
        {
            socid = dto.ClientId.ToString(),
            type = "0",
            statut = InvoiceMapper.ConvertStatusToDolibarr(dto.Status),
            date = ((DateTimeOffset)dto.Date).ToUnixTimeSeconds().ToString(),
            date_lim_reglement = dto.ExpireDate.HasValue
                ? ((DateTimeOffset)dto.ExpireDate.Value).ToUnixTimeSeconds().ToString()
                : null,
            @ref = dto.Reference,
            note_public = dto.NotePublic,
            note_private = dto.NotePrivate,
            lines = dto.Lines.Select(line => new
            {
                desc = line.Description,
                qty = line.Quantity.ToString(CultureInfo.InvariantCulture),
                subprice = line.UnitPrice.ToString(CultureInfo.InvariantCulture),
                tva_tx = line.TaxRate.ToString(CultureInfo.InvariantCulture)
            }).ToArray()
        };

        var responseBody = await apiClient.PostAsync("invoices", requestBody);

        return int.Parse(responseBody);
    }

    public async Task<string> AddInvoiceLineAsync(int invoiceId, CreateInvoiceLineDto lineDto)
    {
        var invoice = await apiClient.GetResourceAsync<InvoiceDetailResponse>($"invoices/{invoiceId}");
        if (invoice.statut != "0") throw new ForbiddenException("Solo se pueden añadir líneas a facturas en borrador");

        var requestBody = new
        {
            desc = lineDto.Description,
            qty = lineDto.Quantity.ToString(CultureInfo.InvariantCulture),
            subprice = lineDto.UnitPrice.ToString(CultureInfo.InvariantCulture),
            tva_tx = lineDto.TaxRate.ToString(CultureInfo.InvariantCulture)
        };

        return await apiClient.PostAsync($"invoices/{invoiceId}/lines", requestBody);
    }

    public async Task UpdateInvoiceAsync(int id, UpdateInvoiceDto dto)
    {
        // GET el JSON completo
        var current = await apiClient.GetResourceAsync<InvoiceDetailResponse>($"invoices/{id}");

        // Modifica solo campos que Dolibarr permite en PUT
        if (dto.Number != null) current.@ref = dto.Number;
        if (dto.NotePublic != null) current.note_public = dto.NotePublic;
        if (dto.NotePrivate != null) current.note_private = dto.NotePrivate;
        if (dto.ExpireDate.HasValue) current.date_lim_reglement = ((DateTimeOffset)dto.ExpireDate.Value).ToUnixTimeSeconds();

        // No tocar: date, socid, lines (Dolibarr no los cambia en PUT)

        await apiClient.PutAsync($"invoices/{id}", current);
    }

    public async Task ChangeInvoiceStatusAsync(int id, string status)
    {
        var normalized = status.Trim().ToLowerInvariant();
        var endpoint = normalized switch
        {
            "draft"  => $"invoices/{id}/settodraft",
            "unpaid" => $"invoices/{id}/settounpaid",
            "paid"   => $"invoices/{id}/settopaid",
            _ => throw new ValidationException("Estado invalido. Usa: draft, unpaid, paid.")
        };

        await apiClient.PostAsync(endpoint, new { });

        // Notificación a Teams/Slack — fire and forget desde el punto de vista del caller
        var invoice = await apiClient.GetResourceAsync<InvoiceDetailResponse>($"invoices/{id}");
        await notifications.NotifyInvoiceStatusChangedAsync(id, invoice.@ref ?? $"#{id}", normalized);
    }

    public async Task ValidateInvoiceAsync(int id)
    {
        var invoice = await apiClient.GetResourceAsync<InvoiceDetailResponse>($"invoices/{id}");
        if (invoice.statut != "0") throw new ForbiddenException("Solo se pueden validar facturas en borrador (draft)");

        await apiClient.PostAsync($"invoices/{id}/validate", new { });

        await notifications.NotifyInvoiceStatusChangedAsync(id, invoice.@ref ?? $"#{id}", "unpaid");
    }

    public async Task DeleteInvoiceAsync(int id)
    {
        var invoice = await apiClient.GetResourceAsync<InvoiceDetailResponse>($"invoices/{id}");
        if (invoice.statut != "0") throw new ForbiddenException("Solo se pueden eliminar facturas en borrador (draft)");

        await apiClient.DeleteAsync($"invoices/{id}");
    }

    public async Task DeleteInvoiceLineAsync(int invoiceId, int lineId)
    {
        var invoice = await apiClient.GetResourceAsync<InvoiceDetailResponse>($"invoices/{invoiceId}");
        if (invoice.statut != "0") throw new ForbiddenException("Solo se pueden eliminar líneas de facturas en borrador (draft)");

        await apiClient.DeleteAsync($"invoices/{invoiceId}/lines/{lineId}");
    }

    public async Task UpdateInvoiceLineAsync(int invoiceId, int lineId, UpdateInvoiceLineDto dto)
    {
        var invoice = await apiClient.GetResourceAsync<InvoiceDetailResponse>($"invoices/{invoiceId}");
        if (invoice.statut != "0") throw new ForbiddenException("Solo se pueden modificar líneas de facturas en borrador (draft)");

        var existingLine = invoice.Lines?.FirstOrDefault(l => l.id == lineId.ToString());
        if (existingLine == null) throw new NotFoundException($"Línea {lineId} no encontrada en factura {invoiceId}");

        var requestBody = new Dictionary<string, object?>
        {
            ["desc"] = dto.Description ?? existingLine.description ?? existingLine.desc,
            ["qty"] = dto.Quantity?.ToString(CultureInfo.InvariantCulture) ?? existingLine.qty,
            ["subprice"] = dto.UnitPrice?.ToString(CultureInfo.InvariantCulture) ?? existingLine.subprice,
            ["tva_tx"] = dto.TaxRate?.ToString(CultureInfo.InvariantCulture) ?? existingLine.tva_tx
        };

        await apiClient.PutAsync($"invoices/{invoiceId}/lines/{lineId}", requestBody);
    }

    public async Task<List<InvoiceDto>> GetInvoiceTemplatesAsync()
    {
        // Dolibarr has no list endpoint for templates — only GET invoices/templates/{id}.
        // FactureRec serializes numeric fields as JSON numbers (not strings), so we use
        // TemplateInvoiceResponse with proper int/decimal types.
        // Non-existent IDs return HTTP 200 with id=0 (Dolibarr quirk), so we filter by id > 0.
        const int maxId = 50;
        var tasks = Enumerable.Range(1, maxId).Select(async id =>
        {
            try { return await apiClient.GetResourceAsync<TemplateInvoiceResponse>($"invoices/templates/{id}"); }
            catch { return null; }
        });

        var results = await Task.WhenAll(tasks);
        var dtos = results
            .Where(r => r is { id: > 0 })
            .Select(r => InvoiceMapper.MapTemplateToInvoiceDto(r!))
            .ToList();

        if (dtos.Count == 0) return dtos;

        var clientIds = dtos.Select(d => d.ClientId).Distinct().ToList();
        var names = await GetClientNamesAsync(clientIds);
        foreach (var dto in dtos)
            dto.ClientName = names.GetValueOrDefault(dto.ClientId);

        return dtos;
    }

    public async Task<InvoiceDetailDto> GetInvoiceTemplateAsync(int id)
    {
        var data = await apiClient.GetResourceAsync<TemplateInvoiceResponse>($"invoices/templates/{id}");
        if (data.id == 0) throw new NotFoundException($"Template invoice {id} not found");
        return InvoiceMapper.MapTemplateToDetailDto(data);
    }

    private async Task<Dictionary<int, string>> GetClientNamesAsync(List<int> clientIds)
    {
        var validIds = clientIds.Distinct().Where(id => id > 0).ToList();
        if (validIds.Count == 0) return [];

        var tasks = validIds.Select(async id =>
        {
            try { return await apiClient.GetResourceAsync<ClientResponse>($"thirdparties/{id}"); }
            catch { return null; }
        });

        var clients = await Task.WhenAll(tasks);
        return clients.Where(c => c is { id: not null })
            .ToDictionary(c => int.Parse(c!.id!), c => c!.name ?? "");
    }

    public async Task<List<InvoicePaymentDto>> GetInvoicePaymentsAsync(int invoiceId)
    {
        var dataList = await apiClient.GetCollectionAsync<InvoicePaymentResponse>($"invoices/{invoiceId}/payments");
        var payments = dataList.Select(InvoiceMapper.MapToInvoicePaymentDto).ToList();

        return payments;
    }
    private async Task NotifyIfPaid(int invoiceId)
    {
        var inv = await apiClient.GetResourceAsync<InvoiceDetailResponse>($"invoices/{invoiceId}");
        var status = inv.statut switch { "2" => "paid", "1" => "unpaid", _ => "draft" };
        await notifications.NotifyInvoiceStatusChangedAsync(invoiceId, inv.@ref ?? $"#{invoiceId}", status);
    }

    public async Task<int> AddInvoicePaymentAsync(int invoiceId, CreateInvoicePaymentDto dto)
    {
        // Usar paymentMethodId si viene, si no usar paymentModeId
        var paymentModeId = dto.PaymentMethodId ?? dto.PaymentModeId;

        var datepaye = new DateTimeOffset(dto.PaymentDate, TimeSpan.Zero).ToUnixTimeSeconds();

        if (dto.Amount.HasValue)
        {
            // Pago parcial: usar /invoices/paymentsdistributed
            var requestBody = new Dictionary<string, object>
            {
                ["arrayofamounts"] = new Dictionary<string, object>
                {
                    {
                        invoiceId.ToString(),
                        new { amount = dto.Amount.Value.ToString(CultureInfo.InvariantCulture) }
                    }
                },
                ["datepaye"]          = datepaye,
                ["paymentid"]         = paymentModeId,
                ["closepaidinvoices"] = dto.ClosePaidInvoices,
                ["accountid"]         = dto.AccountId,
                ["num_payment"]       = dto.PaymentNumber ?? "",
            };
            var responseBody = await apiClient.PostAsync("invoices/paymentsdistributed", requestBody);
            var paymentId = int.Parse(responseBody);
            await NotifyIfPaid(invoiceId);
            return paymentId;
        }
        else
        {
            // Pagar completa pendiente: usar /invoices/{id}/payments
            var requestBody = new Dictionary<string, object>
            {
                ["datepaye"]          = datepaye,
                ["paymentid"]         = paymentModeId,
                ["closepaidinvoices"] = dto.ClosePaidInvoices,
                ["accountid"]         = dto.AccountId,
                ["num_payment"]       = dto.PaymentNumber ?? "",
            };
            var responseBody = await apiClient.PostAsync($"invoices/{invoiceId}/payments", requestBody);
            var paymentId = int.Parse(responseBody);
            await NotifyIfPaid(invoiceId);
            return paymentId;
        }
    }
}
