using GestionaDenuncias.Shared.Models;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

namespace ApiDenuncias.Services
{
    public class GestionaService : IGestionaService
    {
        private readonly HttpClient _http;
        private readonly GestionaOptions _opts;
        private readonly ILogger<GestionaService> _logger;

        public GestionaService(
            HttpClient http,
            IOptions<GestionaOptions> optsAccessor,
            ILogger<GestionaService> logger)
        {
            _http = http;
            _opts = optsAccessor.Value;
            _logger = logger;
        }

        // =========================================================
        // Helpers
        // =========================================================

        private static string BuildFilterViewParam(object filterView)
        {
            var json = JsonSerializer.Serialize(filterView);
            var b64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(json));
            return $"filter-view={Uri.EscapeDataString(b64)}";
        }

        private static string? GetLinkHref(JsonElement item, string rel)
        {
            if (!item.TryGetProperty("links", out var links) || links.ValueKind != JsonValueKind.Array)
            {
                return null;
            }

            foreach (var link in links.EnumerateArray())
            {
                if (link.TryGetProperty("rel", out var relProp) &&
                    string.Equals(relProp.GetString(), rel, StringComparison.OrdinalIgnoreCase) &&
                    link.TryGetProperty("href", out var hrefProp) &&
                    hrefProp.ValueKind == JsonValueKind.String)
                {
                    return hrefProp.GetString();
                }
            }

            return null;
        }

        // Reemplaza este helper si quieres controlar la versión en Accept:
        private void AddTokenAndAccept(HttpRequestMessage req, string mediaType, string? version = null)
        {
            req.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", _opts.AccessToken);
            req.Headers.Accept.Clear();
            if (string.IsNullOrWhiteSpace(version))
            {
                req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(mediaType));
            }
            else
            {
                var mt = new MediaTypeWithQualityHeaderValue(mediaType);
                mt.Parameters.Add(new NameValueHeaderValue("version", version));
                req.Headers.Accept.Add(mt);
            }
        }

        // Helper estilo Postman para /rest/files sin filter-view
        private void AddBasicHeaders(HttpRequestMessage req)
        {
            req.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", _opts.AccessToken);
            req.Headers.Accept.Clear();
            req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
        }

        private void LogDeprecatedHeaders(HttpResponseMessage response, string operation)
        {
            if (response.Headers.TryGetValues("X-Gestiona-Deprecated", out var deprecated))
            {
                _logger.LogWarning(
                    "Gestiona devolvio X-Gestiona-Deprecated en {Operation}: {Deprecated}",
                    operation,
                    string.Join(" | ", deprecated));
            }
        }



        // =========================================================
        // Expedientes (file)
        // =========================================================

        public async Task<GestionaCreateFileResponse> CreateFileAsync(Guid procedureId, string subject, string documentSeries, string siaCode)
        {
            _ = subject;
            _ = documentSeries;
            _ = siaCode;

            var effectiveProcedureId = procedureId == Guid.Empty
                ? Guid.Parse("82722c9b-cecc-4299-8a7b-ce5abeb8170b")
                : procedureId;

            var url = await ResolveExternalProcedureCreateFileUrlAsync(effectiveProcedureId);
            using var req = new HttpRequestMessage(HttpMethod.Post, url);
            req.Headers.Accept.Clear();
            req.Headers.Accept.Add(
                MediaTypeWithQualityHeaderValue.Parse("application/vnd.gestiona.file-opening+json; version=1"));
            req.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", _opts.AccessToken);

            using var resp = await _http.SendAsync(req);
            LogDeprecatedHeaders(resp, "POST create-file");
            var body = await resp.Content.ReadAsStringAsync();
            if (!resp.IsSuccessStatusCode)
                throw new InvalidOperationException($"CreateFileAsync: {(int)resp.StatusCode} {resp.StatusCode}\n{body}");

            using var doc = JsonDocument.Parse(body);
            var fileUrl = GetLinkHref(doc.RootElement, "file")
                ?? throw new InvalidOperationException("CreateFileAsync: Gestiona no ha devuelto link 'file'.");
            var fileOpenUrl = GetLinkHref(doc.RootElement, "file-open")
                ?? throw new InvalidOperationException("CreateFileAsync: Gestiona no ha devuelto link 'file-open'.");

            return new GestionaCreateFileResponse(fileUrl, fileOpenUrl);
        }

        private Task<string> ResolveExternalProcedureCreateFileUrlAsync(Guid procedureId)
        {
            var externalProcedureId = Guid.TryParse(_opts.ExternalProcedureId, out var configuredExternalProcedureId)
                ? configuredExternalProcedureId
                : procedureId;

            return Task.FromResult(
                $"/rest/catalog-2015/procedures/{procedureId}/external-procedures/{externalProcedureId}/create-file");
        }

        public async Task OpenFileAsync(
            string fileUrl,
            string? fileOpenUrl,
            Guid managementUnitGroupId,
            Guid assignedGroupId,
            bool confidential,
            string freeTitle,
            string siaCode)
        {
            if (string.IsNullOrWhiteSpace(fileOpenUrl))
            {
                throw new InvalidOperationException(
                    "OpenFileAsync: falta el link 'file-open' devuelto por Gestiona. No se usa el fallback /open para evitar la ruta deprecated.");
            }

            var url = fileOpenUrl;

            var payload = new
            {
                free_title = freeTitle,
                location = siaCode,
                entry_date = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(),
                confidential,
                initial_assignation = new[]
                {
                    new { rel = "group", href = $"{_opts.ApiBase}/rest/groups/{assignedGroupId}" }
                },
                links = new[]
                {
                    new { rel = "management-unit-group", href = $"{_opts.ApiBase}/rest/groups/{managementUnitGroupId}" }
                }
            };

            var json = JsonSerializer.Serialize(payload);
            using var content = new StringContent(json, Encoding.UTF8, "application/vnd.gestiona.file-opening+json");
            content.Headers.ContentType!.Parameters.Add(new NameValueHeaderValue("version", "1"));

            using var req = new HttpRequestMessage(HttpMethod.Post, url) { Content = content };
            AddTokenAndAccept(req, "application/json");

            using var resp = await _http.SendAsync(req);
            LogDeprecatedHeaders(resp, "POST file-open");
            var body = await resp.Content.ReadAsStringAsync();
            if (!resp.IsSuccessStatusCode)
                throw new InvalidOperationException($"OpenFileAsync: {(int)resp.StatusCode} {resp.StatusCode}\n{body}");
        }

        public async Task<Guid> CreateFolderAsync(string fileUrl, string folderName)
        {
            var endpoint = $"{fileUrl}/documents-and-folders";
            var payload = new { name = folderName };
            var json = JsonSerializer.Serialize(payload);
            using var content = new StringContent(json, Encoding.UTF8, "application/vnd.gestiona.file-folder+json");
            content.Headers.ContentType!.Parameters.Add(new NameValueHeaderValue("version", "1"));

            using var req = new HttpRequestMessage(HttpMethod.Post, endpoint)
            {
                Content = content
            };
            req.Headers.TryAddWithoutValidation("Prefer", "return=minimal");
            req.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", _opts.AccessToken);
            req.Headers.Accept.Clear();

            using var resp = await _http.SendAsync(req);
            LogDeprecatedHeaders(resp, "POST file-folder");
            var body = await resp.Content.ReadAsStringAsync();
            if (!resp.IsSuccessStatusCode)
                throw new InvalidOperationException($"CreateFolderAsync: {(int)resp.StatusCode} {resp.StatusCode}\n{body}");

            var selfHref = TryGetSelfHref(body) ?? resp.Headers.Location?.ToString();
            if (string.IsNullOrWhiteSpace(selfHref))
            {
                throw new InvalidOperationException("CreateFolderAsync: Gestiona no ha devuelto link self ni Location.");
            }

            return Guid.Parse(selfHref.TrimEnd('/').Split('/').Last());
        }

        public async Task<string> CreateUploadAsync(byte[] contentBytes, string fileName)
        {
            using var createReq = new HttpRequestMessage(HttpMethod.Post, "/rest/uploads");
            createReq.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", _opts.AccessToken);
            createReq.Headers.TryAddWithoutValidation("Prefer", "return=minimal");
            createReq.Headers.Accept.Add(
                MediaTypeWithQualityHeaderValue.Parse("application/vnd.gestiona.upload+json"));

            using var createResp = await _http.SendAsync(createReq);
            LogDeprecatedHeaders(createResp, "POST /rest/uploads");
            var createBody = await createResp.Content.ReadAsStringAsync();
            if (!createResp.IsSuccessStatusCode)
                throw new InvalidOperationException($"CreateUpload (POST): {(int)createResp.StatusCode} {createResp.StatusCode}\n{createBody}");

            var uploadUri = createResp.Headers.Location?.ToString()
                            ?? throw new InvalidOperationException("No se devolvió Location en /rest/uploads");

            string md5Hex;
            using (var md5 = MD5.Create())
            {
                var hash = md5.ComputeHash(contentBytes);
                md5Hex = BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
            }

            using var putReq = new HttpRequestMessage(HttpMethod.Put, uploadUri);
            putReq.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", _opts.AccessToken);
            putReq.Headers.TryAddWithoutValidation("X-Gestiona-Upload-MD5", md5Hex);
            putReq.Headers.TryAddWithoutValidation("Slug", fileName);
            putReq.Headers.TryAddWithoutValidation("Prefer", "return=minimal");
            putReq.Headers.Accept.Add(
                MediaTypeWithQualityHeaderValue.Parse("application/vnd.gestiona.upload+json"));
            putReq.Content = new ByteArrayContent(contentBytes);
            putReq.Content.Headers.ContentType = new MediaTypeHeaderValue("application/pdf");

            using var putResp = await _http.SendAsync(putReq);
            LogDeprecatedHeaders(putResp, "PUT upload content");
            var infoJson = await putResp.Content.ReadAsStringAsync();
            if (!putResp.IsSuccessStatusCode)
                throw new InvalidOperationException($"CreateUpload (PUT): {(int)putResp.StatusCode} {putResp.StatusCode}\n{infoJson}");

            if (!string.IsNullOrWhiteSpace(infoJson))
            {
                using var infoDoc = JsonDocument.Parse(infoJson);
                var status = infoDoc.RootElement.TryGetProperty("status", out var statusProp)
                    ? statusProp.GetString()
                    : "READY";
                if (!string.Equals(status, "READY", StringComparison.OrdinalIgnoreCase))
                    throw new InvalidOperationException($"Upload no READY: {status}");
            }
            return uploadUri;
        }

        public async Task UploadDocumentAsync(string fileUrl, byte[] contentBytes, string fileName)
        {
            var uploadUri = await CreateUploadAsync(contentBytes, fileName);

            var metaPayload = new
            {
                type = "DIGITAL",
                name = fileName,
                description = "Denuncia combinada",
                elaboration_state = "EE01",
                metadata_language = "ES",
                links = new[] { new { rel = "content", href = uploadUri } }
            };

            var metaJson = JsonSerializer.Serialize(metaPayload);
            using var metaContent = new StringContent(metaJson, Encoding.UTF8);
            metaContent.Headers.ContentType =
                MediaTypeHeaderValue.Parse("application/vnd.gestiona.file-document+json; version=4");

            using var metaReq = new HttpRequestMessage(HttpMethod.Post, $"{fileUrl}/documents-and-folders")
            { Content = metaContent };
            metaReq.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", _opts.AccessToken);
            metaReq.Headers.TryAddWithoutValidation("Prefer", "return=minimal");
            metaReq.Headers.Accept.Clear();

            using var metaResp = await _http.SendAsync(metaReq);
            LogDeprecatedHeaders(metaResp, "POST file document");
            var body = await metaResp.Content.ReadAsStringAsync();
            if (!metaResp.IsSuccessStatusCode)
                throw new InvalidOperationException($"UploadDocumentAsync: {(int)metaResp.StatusCode} {metaResp.StatusCode}\n{body}");
        }

        private static string? TryGetSelfHref(string body)
        {
            if (string.IsNullOrWhiteSpace(body))
            {
                return null;
            }

            try
            {
                using var doc = JsonDocument.Parse(body);
                return GetLinkHref(doc.RootElement, "self");
            }
            catch
            {
                return null;
            }
        }

        // =========================
        // TERCEROS
        // =========================

        public async Task<(string Id, string SelfHref)> BuscarTerceroPorNifAsync(string nif)
        {
            var filtro = new
            {
                result = new { max_results = 25 },
                filter = new { nif }
            };
            var jsonFiltro = JsonSerializer.Serialize(filtro);
            var b64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(jsonFiltro));
            var url = $"{_opts.ApiBase}/rest/thirds?filter-view={Uri.EscapeDataString(b64)}";

            using var req = new HttpRequestMessage(HttpMethod.Get, url);
            req.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", _opts.AccessToken);
            req.Headers.TryAddWithoutValidation("Accept", "application/vnd.gestiona.thirds-page+json");

            using var resp = await _http.SendAsync(req);
            resp.EnsureSuccessStatusCode();
            var body = await resp.Content.ReadAsStringAsync();

            using var doc = JsonDocument.Parse(body);
            if (!doc.RootElement.TryGetProperty("content", out var content) || content.ValueKind != JsonValueKind.Array)
                return default;

            var match = content.EnumerateArray().FirstOrDefault(e =>
                e.TryGetProperty("nif", out var nifProp) && nifProp.GetString() == nif);

            if (match.ValueKind == JsonValueKind.Undefined) return default;

            var id = match.GetProperty("id").GetString()!;
            var self = match.GetProperty("links").EnumerateArray()
                            .First(l => l.GetProperty("rel").GetString() == "self")
                            .GetProperty("href").GetString()!;
            return (id, self);
        }

        public async Task<(string Id, string SelfHref)> CrearTerceroAsync(ThirdPartyIdentityData thirdParty)
        {
            if (thirdParty is null)
                throw new ArgumentNullException(nameof(thirdParty));

            if (string.IsNullOrWhiteSpace(thirdParty.DocumentId))
                throw new ArgumentException("Documento identificativo obligatorio.", nameof(thirdParty));

            if (thirdParty.IsLegalEntity)
            {
                if (string.IsNullOrWhiteSpace(thirdParty.BusinessName))
                    throw new ArgumentException("La razón social es obligatoria para terceros jurídicos.", nameof(thirdParty));
            }
            else
            {
                if (string.IsNullOrWhiteSpace(thirdParty.FirstName))
                    throw new ArgumentException("Nombre obligatorio.", nameof(thirdParty));
                if (string.IsNullOrWhiteSpace(thirdParty.LastName))
                    throw new ArgumentException("Primer apellido obligatorio.", nameof(thirdParty));
            }

            var payload = new Dictionary<string, object?>
            {
                ["nif_country"] = NormalizeCountryCode(
                    string.IsNullOrWhiteSpace(thirdParty.CountryCode)
                        ? thirdParty.Address?.CountryCode
                        : thirdParty.CountryCode),
                ["nif"] = thirdParty.DocumentId.Trim(),
                ["type"] = thirdParty.IsLegalEntity ? "JURIDICAL" : "PHISIC",
                ["notification_channel"] = BuildNotificationChannel(thirdParty)
            };
            var nifType = GuessNifType(thirdParty.DocumentId, thirdParty.IsLegalEntity);
            if (!string.IsNullOrWhiteSpace(nifType))
            {
                payload["nif_type"] = nifType;
            }

            if (thirdParty.IsLegalEntity)
            {
                payload["business_name"] = thirdParty.BusinessName.Trim();
            }
            else
            {
                var (firstSurname, secondSurname) = SplitSurnames(thirdParty.LastName);
                payload["first_name"] = thirdParty.FirstName.Trim();
                payload["first_surname"] = firstSurname;
                if (!string.IsNullOrWhiteSpace(secondSurname))
                    payload["second_surname"] = secondSurname;
            }

            if (!string.IsNullOrWhiteSpace(thirdParty.Email))
                payload["email"] = thirdParty.Email.Trim();

            var jsonOpts = new JsonSerializerOptions
            {
                DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
            };
            var json = JsonSerializer.Serialize(payload, jsonOpts);

            using var content = new StringContent(json, Encoding.UTF8, "application/vnd.gestiona.third+json");
            content.Headers.ContentType!.Parameters.Add(new NameValueHeaderValue("version", "3"));

            using var req = new HttpRequestMessage(HttpMethod.Post, "/rest/thirds")
            {
                Content = content
            };
            req.Headers.Accept.Clear();
            req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.gestiona.third+json"));
            req.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", _opts.AccessToken);

            using var resp = await _http.SendAsync(req);
            var body = await resp.Content.ReadAsStringAsync();

            if (!resp.IsSuccessStatusCode)
                throw new InvalidOperationException(
                    $"Error CrearTerceroAsync: {(int)resp.StatusCode} {resp.ReasonPhrase}\n{body}");

            using var doc = JsonDocument.Parse(body);
            var id = doc.RootElement.GetProperty("id").GetString()!;
            var selfHref = doc.RootElement.GetProperty("links").EnumerateArray()
                .First(l => l.GetProperty("rel").GetString() == "self")
                .GetProperty("href").GetString()!;

            if (thirdParty.Address?.HasAnyValue == true)
            {
                await TryEnsureThirdAddressAsync(selfHref, thirdParty.Address);
            }

            return (id, selfHref);
        }

        public async Task<HashSet<string>> ObtenerTercerosEnlazadosAsync(string fileUrl)
        {
            using var req = new HttpRequestMessage(HttpMethod.Get, $"{fileUrl}/thirdparties");
            req.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", _opts.AccessToken);
            req.Headers.TryAddWithoutValidation("Accept", "*/*");

            using var resp = await _http.SendAsync(req);
            if (resp.StatusCode == System.Net.HttpStatusCode.NoContent)
                return new HashSet<string>(StringComparer.Ordinal);

            resp.EnsureSuccessStatusCode();
            var body = await resp.Content.ReadAsStringAsync();

            using var doc = JsonDocument.Parse(body);
            var set = new HashSet<string>(StringComparer.Ordinal);
            if (doc.RootElement.TryGetProperty("content", out var content) && content.ValueKind == JsonValueKind.Array)
            {
                foreach (var item in content.EnumerateArray())
                {
                    if (item.TryGetProperty("links", out var links))  
                    {
                        var third = links.EnumerateArray().FirstOrDefault(l => l.GetProperty("rel").GetString() == "third");
                        if (third.ValueKind != JsonValueKind.Undefined)
                            set.Add(third.GetProperty("href").GetString()!);
                    }
                }
            }
            return set;
        }

        public async Task EnlazarTerceroExistenteAsync(string fileUrl, string thirdSelfHref)
        {
            var payload = new { href = thirdSelfHref };
            var json = JsonSerializer.Serialize(payload);
            using var content = new StringContent(json, Encoding.UTF8);
            content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/vnd.gestiona.third-link+json");

            using var req = new HttpRequestMessage(HttpMethod.Post, $"{fileUrl}/thirdparties");
            req.Content = content;
            req.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", _opts.AccessToken);
            req.Headers.TryAddWithoutValidation("Accept", "*/*");

            var resp = await _http.SendAsync(req);
            var body = await resp.Content.ReadAsStringAsync();

            if (!resp.IsSuccessStatusCode)
                throw new InvalidOperationException($"Error EnlazarTerceroExistenteAsync: {resp.StatusCode}\n{body}");
        }

        public async Task AsegurarTerceroYEnlazarAsync(string fileUrl, ThirdPartyIdentityData thirdParty)
        {
            if (thirdParty is null)
                throw new ArgumentNullException(nameof(thirdParty));

            thirdParty = NormalizeThirdParty(thirdParty);

            if (string.IsNullOrWhiteSpace(thirdParty.DocumentId)) return;

            var encontrado = await BuscarTerceroPorNifAsync(thirdParty.DocumentId);

            if (string.IsNullOrEmpty(encontrado.SelfHref))
            {
                if (!CanCreateThirdParty(thirdParty))
                {
                    _logger.LogWarning(
                        "Se omite la creacion/enlace del tercero en Gestiona para el expediente {FileUrl}: datos identificativos incompletos.",
                        fileUrl);
                    return;
                }

                encontrado = await CrearTerceroAsync(thirdParty);
            }
            else if (thirdParty.Address?.HasAnyValue == true)
            {
                await TryEnsureThirdAddressAsync(encontrado.SelfHref, thirdParty.Address);
            }

            var yaEnlazados = await ObtenerTercerosEnlazadosAsync(fileUrl);
            if (!yaEnlazados.Contains(encontrado.SelfHref))
                await EnlazarTerceroExistenteAsync(fileUrl, encontrado.SelfHref);
        }

        private static ThirdPartyIdentityData NormalizeThirdParty(ThirdPartyIdentityData thirdParty)
        {
            if (!thirdParty.IsAnonymous)
            {
                var documentId = (thirdParty.DocumentId ?? string.Empty).Trim().ToUpperInvariant();
                var firstName = (thirdParty.FirstName ?? string.Empty).Trim();
                var lastName = (thirdParty.LastName ?? string.Empty).Trim();
                var businessName = (thirdParty.BusinessName ?? string.Empty).Trim();
                var isLegalEntity = thirdParty.IsLegalEntity ||
                                    LooksLikeLegalEntityDocument(documentId) ||
                                    (!string.IsNullOrWhiteSpace(businessName) &&
                                     string.IsNullOrWhiteSpace(firstName));

                return new ThirdPartyIdentityData
                {
                    IsAnonymous = false,
                    IsLegalEntity = isLegalEntity,
                    DocumentId = documentId,
                    FirstName = isLegalEntity ? string.Empty : firstName,
                    LastName = isLegalEntity ? string.Empty : lastName,
                    BusinessName = businessName,
                    Email = (thirdParty.Email ?? string.Empty).Trim(),
                    CountryCode = string.IsNullOrWhiteSpace(thirdParty.CountryCode) ? "ESP" : thirdParty.CountryCode.Trim(),
                    Address = thirdParty.Address
                };
            }

            return new ThirdPartyIdentityData
            {
                IsAnonymous = true,
                IsLegalEntity = false,
                DocumentId = "00000000T",
                FirstName = "Anonimo",
                LastName = "-",
                BusinessName = string.Empty,
                Email = string.Empty,
                CountryCode = string.IsNullOrWhiteSpace(thirdParty.CountryCode) ? "ESP" : thirdParty.CountryCode,
                Address = null
            };
        }

        private static bool CanCreateThirdParty(ThirdPartyIdentityData thirdParty)
        {
            if (string.IsNullOrWhiteSpace(thirdParty.DocumentId))
            {
                return false;
            }

            return thirdParty.IsLegalEntity
                ? !string.IsNullOrWhiteSpace(thirdParty.BusinessName)
                : !string.IsNullOrWhiteSpace(thirdParty.FirstName) &&
                  !string.IsNullOrWhiteSpace(thirdParty.LastName);
        }

        private static bool LooksLikeLegalEntityDocument(string documentId)
        {
            var value = (documentId ?? string.Empty).Trim().ToUpperInvariant();
            return Regex.IsMatch(value, @"^[ABCDEFGHJKLMNPQRSUVW]\d{7}[A-Z0-9]$");
        }

        // --- CONSULTAS DE EXPEDIENTES (sin recorrer histórico paginado) ---

        private async Task<string> GetFilesAsync(object? filter = null)
        {
            using var req = new HttpRequestMessage(HttpMethod.Get, "/rest/files");
            AddBasicHeaders(req);
            req.Headers.Accept.Clear();
            req.Headers.Accept.Add(
                MediaTypeWithQualityHeaderValue.Parse("application/vnd.gestiona.files-page+json"));

            if (filter is not null)
            {
                var json = JsonSerializer.Serialize(filter);
                req.Content = new StringContent(json, Encoding.UTF8, "application/vnd.gestiona.filter.files+json");
            }

            using var resp = await _http.SendAsync(req);
            LogDeprecatedHeaders(resp, "GET /rest/files");
            if (resp.StatusCode == System.Net.HttpStatusCode.NoContent)
            {
                return "{\"content\":[]}";
            }

            var body = await resp.Content.ReadAsStringAsync();
            if (!resp.IsSuccessStatusCode)
                throw new InvalidOperationException($"GET /rest/files: {(int)resp.StatusCode} {resp.ReasonPhrase}\n{body}");

            if (string.IsNullOrWhiteSpace(body))
            {
                return "{\"content\":[]}";
            }

            return body;
        }

        private static bool TryGetFilesContent(JsonDocument doc, out JsonElement content)
        {
            if (doc.RootElement.TryGetProperty("content", out var contentElement) &&
                contentElement.ValueKind == JsonValueKind.Array)
            {
                content = contentElement;
                return true;
            }

            if (doc.RootElement.ValueKind == JsonValueKind.Array)
            {
                content = doc.RootElement;
                return true;
            }

            content = default;
            return false;
        }

        /// <summary>
        /// Devuelve el JSON crudo de /rest/files acumulando hasta maxPages páginas.
        /// </summary>
        public async Task<string> ListarExpedientesJsonAsyncBasico(int maxPages = 1)
        {
            _ = maxPages;

            var json = await GetFilesAsync();
            using var doc = JsonDocument.Parse(json);

            if (!TryGetFilesContent(doc, out var content))
            {
                return "[]";
            }

            var sb = new StringBuilder();
            sb.Append('[');

            var first = true;
            foreach (var item in content.EnumerateArray())
            {
                if (!first)
                {
                    sb.Append(',');
                }

                sb.Append(item.GetRawText());
                first = false;
            }

            sb.Append(']');
            return sb.ToString();
        }

        /// <summary>
        /// Busca un expediente cuyo asunto sea "Denuncia {idDenuncia}-CD".
        /// </summary>
        public async Task<GestionaExpedienteInfo?> BuscarExpedientePorIdEnAsuntoAsync(int idDenuncia)
        {
            var needle = $"Denuncia {idDenuncia}-CD";

            var json = await GetFilesAsync(new
            {
                subject = needle
            });
            using var doc = JsonDocument.Parse(json);

            if (!TryGetFilesContent(doc, out var content) || content.GetArrayLength() == 0)
            {
                return null;
            }

            foreach (var item in content.EnumerateArray())
            {
                var expediente = BuildExpedienteInfo(item);
                if (expediente is not null)
                {
                    return expediente;
                }
            }

            return null;
        }

        public async Task<GestionaExpedienteInfo?> ObtenerExpedienteAsync(string fileUrl)
        {
            if (string.IsNullOrWhiteSpace(fileUrl))
            {
                return null;
            }

            using var req = new HttpRequestMessage(HttpMethod.Get, fileUrl);
            AddTokenAndAccept(req, "application/vnd.gestiona.file+json", "2");

            using var resp = await _http.SendAsync(req);
            LogDeprecatedHeaders(resp, $"GET {fileUrl}");
            if (resp.StatusCode == System.Net.HttpStatusCode.NoContent ||
                resp.StatusCode == System.Net.HttpStatusCode.NotFound)
            {
                return null;
            }

            var body = await resp.Content.ReadAsStringAsync();
            if (!resp.IsSuccessStatusCode)
            {
                throw new InvalidOperationException(
                    $"ObtenerExpedienteAsync: {(int)resp.StatusCode} {resp.ReasonPhrase}\n{body}");
            }

            if (string.IsNullOrWhiteSpace(body))
            {
                return null;
            }

            using var doc = JsonDocument.Parse(body);
            return BuildExpedienteInfo(doc.RootElement, fileUrl);
        }

        private static GestionaExpedienteInfo? BuildExpedienteInfo(JsonElement item, string? fallbackFileUrl = null)
        {
            var code = GetJsonString(item, "code");
            var freeTitle = GetJsonString(item, "free_title");
            var subject = GetJsonString(item, "subject");
            var selectableTitle = GetJsonString(item, "selectable_title");
            var procedureName = GetJsonString(item, "procedure_name");

            string? fileUrl = fallbackFileUrl;
            if (item.TryGetProperty("links", out var links) && links.ValueKind == JsonValueKind.Array)
            {
                foreach (var link in links.EnumerateArray())
                {
                    if (link.TryGetProperty("rel", out var rel) &&
                        string.Equals(rel.GetString(), "file", StringComparison.OrdinalIgnoreCase) &&
                        link.TryGetProperty("href", out var href) &&
                        href.ValueKind == JsonValueKind.String)
                    {
                        fileUrl = href.GetString();
                        break;
                    }
                }
            }

            if (string.IsNullOrWhiteSpace(fileUrl) &&
                item.TryGetProperty("id", out var idProp) &&
                idProp.ValueKind == JsonValueKind.String)
            {
                fileUrl = $"/rest/files/{idProp.GetString()}";
            }

            if (string.IsNullOrWhiteSpace(fileUrl))
            {
                return null;
            }

            return new GestionaExpedienteInfo
            {
                FileUrl = fileUrl,
                CodigoExpediente = code,
                FreeTitle = FirstNonEmpty(freeTitle, subject, selectableTitle, procedureName)
            };
        }


        public async Task<List<ExpedienteTerceroDto>> ObtenerExpedientesPorTerceroAsync(
            string nif,
            DateTimeOffset? desde = null,
            DateTimeOffset? hasta = null,
            int maxPages = 1,
            int maxResults = 30,
            int maxParallel = 6 // de momento NO se usa, dejamos la firma por compatibilidad
        )
        {
            if (string.IsNullOrWhiteSpace(nif))
                throw new ArgumentException("NIF obligatorio.", nameof(nif));

            nif = nif.Trim().ToUpperInvariant();
            _ = maxPages;
            _ = maxParallel;

            // 1) Localizar el tercero por NIF
            var tercero = await BuscarTerceroPorNifAsync(nif);
            if (string.IsNullOrEmpty(tercero.SelfHref))
                return new List<ExpedienteTerceroDto>(); // no hay tercero => no hay expedientes

            var resultados = new List<ExpedienteTerceroDto>();
            var json = await GetFilesAsync(new
            {
                third_rest_link = new
                {
                    rel = "third",
                    href = tercero.SelfHref
                }
            });
            using var doc = JsonDocument.Parse(json);

            if (!TryGetFilesContent(doc, out var content) || content.GetArrayLength() == 0)
            {
                return resultados;
            }

            foreach (var item in content.EnumerateArray())
            {
                if (resultados.Count >= maxResults)
                {
                    break;
                }

                DateTimeOffset? creation = null;
                if (item.TryGetProperty("creation_date", out var pCreation))
                {
                    if (pCreation.ValueKind == JsonValueKind.Number &&
                        pCreation.TryGetInt64(out var ts))
                    {
                        creation = DateTimeOffset.FromUnixTimeSeconds(ts);
                    }
                    else if (pCreation.ValueKind == JsonValueKind.String &&
                             long.TryParse(pCreation.GetString(), out var tsString))
                    {
                        creation = DateTimeOffset.FromUnixTimeSeconds(tsString);
                    }
                }

                if (desde.HasValue && creation.HasValue && creation.Value < desde.Value)
                    continue;

                if (hasta.HasValue && creation.HasValue && creation.Value > hasta.Value)
                    continue;

                string? fileUrl = null;
                if (item.TryGetProperty("links", out var links) && links.ValueKind == JsonValueKind.Array)
                {
                    foreach (var l in links.EnumerateArray())
                    {
                        if (l.TryGetProperty("rel", out var relProp) &&
                            string.Equals(relProp.GetString(), "file", StringComparison.OrdinalIgnoreCase) &&
                            l.TryGetProperty("href", out var hrefProp))
                        {
                            fileUrl = hrefProp.GetString();
                            break;
                        }
                    }
                }

                if (string.IsNullOrWhiteSpace(fileUrl) &&
                    item.TryGetProperty("id", out var idProp) &&
                    idProp.ValueKind == JsonValueKind.String)
                {
                    fileUrl = $"/rest/files/{idProp.GetString()}";
                }

                if (string.IsNullOrWhiteSpace(fileUrl))
                    continue;

                var code = GetJsonString(item, "code");
                var subject = GetJsonString(item, "subject");
                var freeTitle = GetJsonString(item, "free_title");
                var selectableTitle = GetJsonString(item, "selectable_title");
                var procedureName = GetJsonString(item, "procedure_name");
                var asunto = FirstNonEmpty(freeTitle, subject, selectableTitle, procedureName);

                string? state = null;
                if (item.TryGetProperty("state", out var pState) && pState.ValueKind == JsonValueKind.String)
                    state = pState.GetString();
                else if (item.TryGetProperty("status", out var pStatus) && pStatus.ValueKind == JsonValueKind.String)
                    state = pStatus.GetString();

                resultados.Add(new ExpedienteTerceroDto
                {
                    FileUrl = fileUrl,
                    CodigoExpediente = code,
                    Asunto = asunto,
                    FechaCreacion = creation,
                    Estado = state
                });
            }

            return resultados;
        }

        private async Task TryEnsureThirdAddressAsync(string thirdSelfHref, ThirdPartyAddressData address)
        {
            if (!address.HasAnyValue)
            {
                return;
            }

            if (await ThirdHasAddressesAsync(thirdSelfHref))
            {
                return;
            }

            var addressPayload = await BuildAddressPayloadAsync(address);
            if (addressPayload is null)
            {
                return;
            }

            var payload = new Dictionary<string, object?>
            {
                ["content"] = new[] { addressPayload }
            };

            var json = JsonSerializer.Serialize(payload, new JsonSerializerOptions
            {
                DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
            });

            using var content = new StringContent(json, Encoding.UTF8, "application/vnd.gestiona.third-addresses+json");
            content.Headers.ContentType!.Parameters.Add(new NameValueHeaderValue("version", "2"));

            using var req = new HttpRequestMessage(HttpMethod.Put, $"{thirdSelfHref.TrimEnd('/')}/addresses")
            {
                Content = content
            };
            req.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", _opts.AccessToken);
            req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

            using var resp = await _http.SendAsync(req);
            var body = await resp.Content.ReadAsStringAsync();
            if (!resp.IsSuccessStatusCode)
                throw new InvalidOperationException($"Error actualizando dirección del tercero: {(int)resp.StatusCode} {resp.ReasonPhrase}\n{body}");
        }

        private async Task<bool> ThirdHasAddressesAsync(string thirdSelfHref)
        {
            using var req = new HttpRequestMessage(HttpMethod.Get, $"{thirdSelfHref.TrimEnd('/')}/addresses");
            req.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", _opts.AccessToken);
            req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

            using var resp = await _http.SendAsync(req);
            if (resp.StatusCode == System.Net.HttpStatusCode.NoContent ||
                resp.StatusCode == System.Net.HttpStatusCode.NotFound)
            {
                return false;
            }

            resp.EnsureSuccessStatusCode();
            var body = await resp.Content.ReadAsStringAsync();
            using var doc = JsonDocument.Parse(body);
            return doc.RootElement.TryGetProperty("content", out var content) &&
                   content.ValueKind == JsonValueKind.Array &&
                   content.GetArrayLength() > 0;
        }

        private async Task<Dictionary<string, object?>?> BuildAddressPayloadAsync(ThirdPartyAddressData address)
        {
            var provinceCode = await ResolveProvinceCodeAsync(address.Province, NormalizeCountryCode(address.CountryCode));
            var townHref = await ResolveTownHrefAsync(provinceCode, address.Municipality);

            if (string.IsNullOrWhiteSpace(address.Street) &&
                string.IsNullOrWhiteSpace(address.Municipality) &&
                string.IsNullOrWhiteSpace(address.ZipCode))
            {
                return null;
            }

            var payload = new Dictionary<string, object?>
            {
                ["address"] = address.Street,
                ["number"] = address.Number,
                ["floor"] = address.Floor,
                ["door"] = address.Door,
                ["block"] = address.Block,
                ["stair"] = address.Stair,
                ["zipcode"] = address.ZipCode,
                ["country"] = NormalizeCountryCode(address.CountryCode),
                ["province"] = provinceCode,
                ["type_of_road"] = string.IsNullOrWhiteSpace(address.RoadTypeCode) ? "CL" : address.RoadTypeCode,
                ["default_address"] = true,
            };

            if (!string.IsNullOrWhiteSpace(townHref))
            {
                payload["links"] = new[]
                {
                    new Dictionary<string, string>
                    {
                        ["rel"] = "town",
                        ["href"] = townHref
                    }
                };
            }
            else if (!string.IsNullOrWhiteSpace(address.Municipality))
            {
                payload["zone"] = address.Municipality.Trim();
            }

            return payload;
        }

        private async Task<string?> ResolveProvinceCodeAsync(string? province, string countryCode)
        {
            if (string.IsNullOrWhiteSpace(province))
            {
                return null;
            }

            var normalizedSearch = NormalizeKey(province);
            using var req = new HttpRequestMessage(HttpMethod.Get, "/rest/provinces");
            req.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", _opts.AccessToken);
            req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

            using var resp = await _http.SendAsync(req);
            resp.EnsureSuccessStatusCode();
            var body = await resp.Content.ReadAsStringAsync();
            using var doc = JsonDocument.Parse(body);

            var collection = doc.RootElement.TryGetProperty("content", out var content) &&
                             content.ValueKind == JsonValueKind.Array
                ? content
                : doc.RootElement;

            if (collection.ValueKind != JsonValueKind.Array)
            {
                return province.Trim().ToUpperInvariant();
            }

            foreach (var item in collection.EnumerateArray())
            {
                var code = item.TryGetProperty("code", out var codeProp) ? codeProp.GetString() : null;
                var name = item.TryGetProperty("name", out var nameProp) ? nameProp.GetString() : null;
                var itemCountry = item.TryGetProperty("country_code", out var countryProp) ? countryProp.GetString() : null;

                if (!string.IsNullOrWhiteSpace(itemCountry) &&
                    !string.Equals(NormalizeCountryCode(itemCountry), countryCode, StringComparison.OrdinalIgnoreCase))
                {
                    continue;
                }

                if (NormalizeKey(code) == normalizedSearch || NormalizeKey(name) == normalizedSearch)
                {
                    return code ?? province.Trim().ToUpperInvariant();
                }
            }

            return province.Trim().ToUpperInvariant();
        }

        private async Task<string?> ResolveTownHrefAsync(string? provinceCode, string? municipality)
        {
            if (string.IsNullOrWhiteSpace(provinceCode) || string.IsNullOrWhiteSpace(municipality))
            {
                return null;
            }

            using var req = new HttpRequestMessage(HttpMethod.Get, $"/rest/provinces/{Uri.EscapeDataString(provinceCode)}/towns");
            req.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", _opts.AccessToken);
            req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

            using var resp = await _http.SendAsync(req);
            if (!resp.IsSuccessStatusCode)
            {
                return null;
            }

            var body = await resp.Content.ReadAsStringAsync();
            using var doc = JsonDocument.Parse(body);

            var collection = doc.RootElement.TryGetProperty("content", out var content) &&
                             content.ValueKind == JsonValueKind.Array
                ? content
                : doc.RootElement;

            if (collection.ValueKind != JsonValueKind.Array)
            {
                return null;
            }

            var normalizedSearch = NormalizeKey(municipality);
            foreach (var item in collection.EnumerateArray())
            {
                var name = item.TryGetProperty("name", out var nameProp) ? nameProp.GetString() : null;
                if (NormalizeKey(name) != normalizedSearch)
                {
                    continue;
                }

                if (!item.TryGetProperty("links", out var links) || links.ValueKind != JsonValueKind.Array)
                {
                    continue;
                }

                foreach (var link in links.EnumerateArray())
                {
                    if (link.TryGetProperty("rel", out var relProp) &&
                        string.Equals(relProp.GetString(), "self", StringComparison.OrdinalIgnoreCase) &&
                        link.TryGetProperty("href", out var hrefProp))
                    {
                        return hrefProp.GetString();
                    }
                }
            }

            return null;
        }

        private static string? GetJsonString(JsonElement item, string propertyName)
        {
            return item.TryGetProperty(propertyName, out var property) &&
                   property.ValueKind == JsonValueKind.String
                ? property.GetString()
                : null;
        }

        private static string? FirstNonEmpty(params string?[] values)
        {
            foreach (var value in values)
            {
                if (!string.IsNullOrWhiteSpace(value))
                {
                    return value;
                }
            }

            return null;
        }

        private static (string FirstSurname, string? SecondSurname) SplitSurnames(string apellidos)
        {
            var parts = (apellidos ?? string.Empty)
                .Split(' ', StringSplitOptions.RemoveEmptyEntries);

            if (parts.Length == 0)
            {
                return ("-", null);
            }

            if (parts.Length == 1)
            {
                return (parts[0], null);
            }

            return (parts[0], string.Join(' ', parts.Skip(1)));
        }

        private static string BuildNotificationChannel(ThirdPartyIdentityData thirdParty)
        {
            _ = thirdParty;
            return "TELEMATIC";
        }

        private static string? GuessNifType(string nif, bool isLegalEntity)
        {
            var value = (nif ?? string.Empty).Trim().ToUpperInvariant();
            if (isLegalEntity && Regex.IsMatch(value, @"^[A-Z]\d{7}[A-Z0-9]$"))
            {
                return "CIF";
            }

            if (value.StartsWith("X", StringComparison.Ordinal) ||
                value.StartsWith("Y", StringComparison.Ordinal) ||
                value.StartsWith("Z", StringComparison.Ordinal))
            {
                return "NIE";
            }

            if (Regex.IsMatch(value, @"^\d{7,8}[A-Z0-9]$"))
            {
                return "NIF";
            }

            return null;
        }

        private static string NormalizeCountryCode(string? country)
        {
            var value = NormalizeKey(country);
            return value switch
            {
                "" => "ESP",
                "es" or "esp" or "espana" or "españa" or "spain" => "ESP",
                "prt" or "pt" or "portugal" => "PRT",
                _ when country is { Length: >= 3 } => country.Trim().ToUpperInvariant()[..3],
                _ => "ESP",
            };
        }

        private static string NormalizeKey(string? value)
        {
            if (string.IsNullOrWhiteSpace(value))
            {
                return string.Empty;
            }

            var normalized = value.Normalize(NormalizationForm.FormD);
            var builder = new StringBuilder(normalized.Length);

            foreach (var character in normalized)
            {
                var category = CharUnicodeInfo.GetUnicodeCategory(character);
                if (category == UnicodeCategory.NonSpacingMark)
                {
                    continue;
                }

                builder.Append(char.IsLetterOrDigit(character) ? char.ToUpperInvariant(character) : ' ');
            }

            return string.Join(' ', builder.ToString().Split(' ', StringSplitOptions.RemoveEmptyEntries));
        }



    }
}
