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;
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(string subject, string documentSeries, string siaCode)
        {
            _ = subject;
            _ = documentSeries;

            var url = await ResolveExternalProcedureCreateFileUrlAsync(siaCode);
            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 async Task<string> ResolveExternalProcedureCreateFileUrlAsync(string siaCode)
        {
            var proceduresUrl = await ResolveRootLinkHrefAsync("vnd.gestiona.catalog-2015")
                ?? "/rest/catalog-2015/procedures";
            var procedures = await GetContentArrayAsync(
                proceduresUrl,
                "application/vnd.gestiona.procedures-2015-page+json",
                "GET catalogo de procedimientos Gestiona");

            var procedure = SelectProcedure(procedures, siaCode);
            var externalProceduresUrl = GetLinkHref(procedure, "external-procedures")
                ?? throw new InvalidOperationException(
                    "El procedimiento seleccionado no contiene link 'external-procedures'.");
            var externalProcedures = await GetContentArrayAsync(
                externalProceduresUrl,
                "application/vnd.gestiona.external-procedures-2015-page+json",
                "GET procedimientos externos Gestiona");

            var externalProcedure = SelectExternalProcedure(externalProcedures, procedure, siaCode);
            return GetLinkHref(externalProcedure, "create-file")
                ?? throw new InvalidOperationException(
                    "El procedimiento externo seleccionado no contiene link 'create-file'.");
        }

        private async Task<string?> ResolveRootLinkHrefAsync(string rel)
        {
            using var req = new HttpRequestMessage(HttpMethod.Get, "/rest");
            AddBasicHeaders(req);

            using var resp = await _http.SendAsync(req);
            LogDeprecatedHeaders(resp, "GET /rest");
            var body = await resp.Content.ReadAsStringAsync();
            if (!resp.IsSuccessStatusCode)
            {
                throw new InvalidOperationException($"ResolveRootLinkHrefAsync: {(int)resp.StatusCode} {resp.StatusCode}\n{body}");
            }

            using var doc = JsonDocument.Parse(body);
            return ToAbsoluteGestionaHref(GetLinkHref(doc.RootElement, rel));
        }

        private async Task<List<JsonElement>> GetContentArrayAsync(string url, string accept, string operation)
        {
            using var req = new HttpRequestMessage(HttpMethod.Get, url);
            req.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", _opts.AccessToken);
            req.Headers.Accept.Clear();
            req.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(accept));

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

            using var doc = JsonDocument.Parse(body);
            var source = doc.RootElement.TryGetProperty("content", out var content) &&
                         content.ValueKind == JsonValueKind.Array
                ? content
                : doc.RootElement;

            if (source.ValueKind != JsonValueKind.Array)
            {
                return [];
            }

            return source.EnumerateArray().Select(item => item.Clone()).ToList();
        }

        private JsonElement SelectProcedure(IReadOnlyList<JsonElement> procedures, string siaCode)
        {
            if (procedures.Count == 0)
            {
                throw new InvalidOperationException("Gestiona no ha devuelto procedimientos en el catalogo.");
            }

            var configuredName = NormalizeKey(_opts.ProcedureName);
            if (!string.IsNullOrWhiteSpace(configuredName))
            {
                var match = procedures.FirstOrDefault(item =>
                    NormalizeKey(GetJsonString(item, "name")).Contains(configuredName, StringComparison.Ordinal) ||
                    NormalizeKey(GetJsonString(item, "title")).Contains(configuredName, StringComparison.Ordinal) ||
                    NormalizeKey(GetJsonString(item, "description")).Contains(configuredName, StringComparison.Ordinal));

                if (match.ValueKind != JsonValueKind.Undefined)
                {
                    return match;
                }

                throw new InvalidOperationException(
                    $"No se ha encontrado en Gestiona el procedimiento configurado '{_opts.ProcedureName}'. Procedimientos disponibles: {DescribeCatalogItems(procedures)}.");
            }

            var normalizedSia = NormalizeKey(siaCode);
            if (!string.IsNullOrWhiteSpace(normalizedSia))
            {
                var match = procedures.FirstOrDefault(item =>
                    NormalizeKey(GetJsonString(item, "sia_code")) == normalizedSia ||
                    NormalizeKey(GetJsonString(item, "code")) == normalizedSia);

                if (match.ValueKind != JsonValueKind.Undefined)
                {
                    return match;
                }
            }

            if (procedures.Count == 1)
            {
                return procedures[0];
            }

            throw new InvalidOperationException(
                "Falta configurar Gestiona:ProcedureName para seleccionar el procedimiento sin usar IDs fijos.");
        }

        private JsonElement SelectExternalProcedure(
            IReadOnlyList<JsonElement> externalProcedures,
            JsonElement selectedProcedure,
            string siaCode)
        {
            var withCreateFile = externalProcedures
                .Where(item => !string.IsNullOrWhiteSpace(GetLinkHref(item, "create-file")))
                .ToList();

            if (withCreateFile.Count == 0)
            {
                throw new InvalidOperationException("El procedimiento seleccionado no tiene procedimientos externos con link 'create-file'.");
            }

            var configuredSia = NormalizeKey(
                string.IsNullOrWhiteSpace(_opts.ExternalProcedureSiaCode)
                    ? siaCode
                    : _opts.ExternalProcedureSiaCode);
            if (!string.IsNullOrWhiteSpace(configuredSia))
            {
                var match = withCreateFile.FirstOrDefault(item =>
                    NormalizeKey(GetJsonString(item, "sia_code")) == configuredSia ||
                    NormalizeKey(GetJsonString(item, "code")) == configuredSia);

                if (match.ValueKind != JsonValueKind.Undefined)
                {
                    return match;
                }
            }

            var configuredName = NormalizeKey(_opts.ExternalProcedureName);
            if (!string.IsNullOrWhiteSpace(configuredName))
            {
                var match = withCreateFile.FirstOrDefault(item =>
                    NormalizeKey(GetJsonString(item, "name")).Contains(configuredName, StringComparison.Ordinal) ||
                    NormalizeKey(GetJsonString(item, "title")).Contains(configuredName, StringComparison.Ordinal));

                if (match.ValueKind != JsonValueKind.Undefined)
                {
                    return match;
                }
            }

            var procedureId = GetJsonString(selectedProcedure, "id");
            if (!string.IsNullOrWhiteSpace(procedureId))
            {
                var sameId = withCreateFile.FirstOrDefault(item =>
                    string.Equals(GetJsonString(item, "id"), procedureId, StringComparison.OrdinalIgnoreCase));
                if (sameId.ValueKind != JsonValueKind.Undefined)
                {
                    return sameId;
                }
            }

            if (withCreateFile.Count == 1)
            {
                return withCreateFile[0];
            }

            throw new InvalidOperationException(
                $"No se puede seleccionar de forma inequivoca el procedimiento externo. Configura Gestiona:ExternalProcedureName o Gestiona:ExternalProcedureSiaCode. Disponibles: {DescribeCatalogItems(withCreateFile)}.");
        }

        private async Task<string> ResolveAssignableGroupHrefAsync(string groupCode)
        {
            if (string.IsNullOrWhiteSpace(groupCode))
            {
                throw new InvalidOperationException("Debe indicarse el codigo funcional del grupo de Gestiona.");
            }

            var groupsUrl = await ResolveRootLinkHrefAsync("vnd.gestiona.files.assignees.groups")
                ?? "/rest/files/assignees/groups";
            var groups = await GetContentArrayAsync(
                groupsUrl,
                "application/vnd.gestiona.groups-page+json",
                "GET grupos asignables Gestiona");

            var normalizedCode = NormalizeKey(groupCode);
            foreach (var group in groups)
            {
                var code = NormalizeKey(GetJsonString(group, "code"));
                var name = NormalizeKey(GetJsonString(group, "name"));
                var title = NormalizeKey(GetJsonString(group, "title"));

                if (code == normalizedCode ||
                    name == normalizedCode ||
                    title == normalizedCode ||
                    name.StartsWith(normalizedCode + " ", StringComparison.Ordinal) ||
                    title.StartsWith(normalizedCode + " ", StringComparison.Ordinal))
                {
                    var href = ToAbsoluteGestionaHref(GetLinkHref(group, "self"));
                    if (!string.IsNullOrWhiteSpace(href))
                    {
                        return href!;
                    }
                }
            }

            throw new InvalidOperationException(
                $"No se ha encontrado en Gestiona el grupo asignable '{groupCode}'. Grupos disponibles: {DescribeCatalogItems(groups)}.");
        }

        private string? ToAbsoluteGestionaHref(string? href)
        {
            if (string.IsNullOrWhiteSpace(href))
            {
                return null;
            }

            if (Uri.TryCreate(href, UriKind.Absolute, out _))
            {
                return href;
            }

            return href.StartsWith("/", StringComparison.Ordinal)
                ? $"{_opts.ApiBase.TrimEnd('/')}{href}"
                : href;
        }

        private static string DescribeCatalogItems(IEnumerable<JsonElement> items)
        {
            var values = items
                .Select(item => FirstNonEmpty(
                    GetJsonString(item, "name"),
                    GetJsonString(item, "title"),
                    GetJsonString(item, "code"),
                    GetJsonString(item, "sia_code"),
                    GetJsonString(item, "id")))
                .Where(value => !string.IsNullOrWhiteSpace(value))
                .Take(10)
                .ToList();

            return values.Count == 0 ? "(sin nombres disponibles)" : string.Join("; ", values);
        }

        public async Task OpenFileAsync(
            string fileUrl,
            string? fileOpenUrl,
            string assignedGroupCode,
            bool confidential,
            string freeTitle)
        {
            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 assignedGroupHref = await ResolveAssignableGroupHrefAsync(assignedGroupCode);
            var managementGroupCode = string.IsNullOrWhiteSpace(_opts.ManagementUnitGroupCode)
                ? "700"
                : _opts.ManagementUnitGroupCode.Trim();
            var managementGroupHref = await ResolveAssignableGroupHrefAsync(managementGroupCode);

            var payload = new
            {
                free_title = freeTitle,
                entry_date = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(),
                confidential,
                initial_assignation = new[]
                {
                    new { rel = "group", href = assignedGroupHref }
                },
                links = new[]
                {
                    new { rel = "management-unit-group", href = managementGroupHref }
                }
            };

            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 AssignFileAsync(string fileUrl, string assignedGroupCode)
        {
            if (string.IsNullOrWhiteSpace(fileUrl))
            {
                throw new InvalidOperationException("AssignFileAsync: falta la URL del expediente.");
            }

            if (string.IsNullOrWhiteSpace(assignedGroupCode))
            {
                throw new InvalidOperationException("AssignFileAsync: falta el grupo asignado.");
            }

            var assigneesUrl = await ResolveFileAssigneesUrlAsync(fileUrl);
            var assignedGroupHref = await ResolveAssignableGroupHrefAsync(assignedGroupCode);
            var payload = new
            {
                links = new[]
                {
                    new { rel = "group", href = assignedGroupHref }
                }
            };
            var json = JsonSerializer.Serialize(payload);
            using var content = new StringContent(json, Encoding.UTF8, "application/vnd.gestiona.links");

            using var req = new HttpRequestMessage(HttpMethod.Put, assigneesUrl)
            {
                Content = content
            };
            req.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", _opts.AccessToken);
            req.Headers.Accept.Clear();
            req.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/vnd.gestiona.links"));

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

        private async Task<string> ResolveFileAssigneesUrlAsync(string fileUrl)
        {
            var normalizedFileUrl = fileUrl.TrimEnd('/');
            using var req = new HttpRequestMessage(HttpMethod.Get, normalizedFileUrl);
            AddTokenAndAccept(req, "application/vnd.gestiona.file+json", "2");

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

            using var doc = JsonDocument.Parse(body);
            return ToAbsoluteGestionaHref(GetLinkHref(doc.RootElement, "assignees"))
                   ?? $"{normalizedFileUrl}/assignees";
        }

        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
            {
                nif
            };

            using var req = new HttpRequestMessage(HttpMethod.Get, "/rest/thirds");
            req.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", _opts.AccessToken);
            req.Headers.TryAddWithoutValidation("Accept", "application/vnd.gestiona.thirds-page+json");
            req.Content = new StringContent(
                JsonSerializer.Serialize(filtro),
                Encoding.UTF8,
                "application/vnd.gestiona.filter.thirds+json");

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

            if (resp.StatusCode == System.Net.HttpStatusCode.NoContent ||
                string.IsNullOrWhiteSpace(body))
            {
                return default;
            }

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

            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);

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

            if (string.IsNullOrWhiteSpace(body))
                return new HashSet<string>(StringComparer.Ordinal);

            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<GestionaEnsureThirdResponse> AsegurarTerceroYEnlazarAsync(string fileUrl, ThirdPartyIdentityData thirdParty)
        {
            if (thirdParty is null)
                throw new ArgumentNullException(nameof(thirdParty));

            thirdParty = NormalizeThirdParty(thirdParty);
            var warnings = new List<string>();

            if (string.IsNullOrWhiteSpace(thirdParty.DocumentId))
            {
                return new GestionaEnsureThirdResponse(true, warnings);
            }

            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 new GestionaEnsureThirdResponse(true, warnings);
                }

                encontrado = await CrearTerceroAsync(thirdParty);
            }
            else
            {
                warnings.AddRange(await BuildThirdPartyDifferenceWarningsAsync(encontrado.SelfHref, thirdParty));
                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);

            return new GestionaEnsureThirdResponse(true, warnings);
        }

        private async Task<List<string>> BuildThirdPartyDifferenceWarningsAsync(
            string thirdSelfHref,
            ThirdPartyIdentityData thirdParty)
        {
            var warnings = new List<string>();
            var details = await GetThirdPartyDetailsAsync(thirdSelfHref);
            if (details is null)
            {
                return warnings;
            }

            var gestionaType = GetJsonString(details.Value, "type");
            var expectedType = thirdParty.IsLegalEntity ? "JURIDICAL" : "PHISIC";
            AddThirdDifferenceWarning(warnings, "tipo de tercero", expectedType, gestionaType, normalizeAsCode: true);

            if (thirdParty.IsLegalEntity)
            {
                AddThirdDifferenceWarning(
                    warnings,
                    "razon social",
                    thirdParty.BusinessName,
                    GetJsonString(details.Value, "business_name"));
            }
            else
            {
                AddThirdDifferenceWarning(
                    warnings,
                    "nombre",
                    thirdParty.FirstName,
                    GetJsonString(details.Value, "first_name"));

                var gestionaLastName = string.Join(
                    ' ',
                    new[]
                    {
                        GetJsonString(details.Value, "first_surname"),
                        GetJsonString(details.Value, "second_surname")
                    }.Where(value => !string.IsNullOrWhiteSpace(value)));
                AddThirdDifferenceWarning(warnings, "apellidos", thirdParty.LastName, gestionaLastName);
            }

            AddThirdDifferenceWarning(warnings, "email", thirdParty.Email, GetJsonString(details.Value, "email"));
            AddThirdDifferenceWarning(
                warnings,
                "pais del documento",
                NormalizeCountryCode(thirdParty.CountryCode),
                GetJsonString(details.Value, "nif_country"),
                normalizeAsCode: true);
            AddThirdDifferenceWarning(
                warnings,
                "canal de notificacion",
                BuildNotificationChannel(thirdParty),
                GetJsonString(details.Value, "notification_channel"),
                normalizeAsCode: true);

            if (warnings.Count > 0)
            {
                warnings.Insert(
                    0,
                    "El tercero ya existia en Gestiona y no se han sustituido sus datos. Si alguno debe cambiarse, actualizalo manualmente en Gestiona.");
            }

            return warnings;
        }

        private async Task<JsonElement?> GetThirdPartyDetailsAsync(string thirdSelfHref)
        {
            if (string.IsNullOrWhiteSpace(thirdSelfHref))
            {
                return null;
            }

            using var req = new HttpRequestMessage(HttpMethod.Get, thirdSelfHref);
            req.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", _opts.AccessToken);
            req.Headers.Accept.Clear();
            req.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/vnd.gestiona.third+json; version=3"));

            using var resp = await _http.SendAsync(req);
            LogDeprecatedHeaders(resp, "GET third");
            if (!resp.IsSuccessStatusCode)
            {
                return null;
            }

            var body = await resp.Content.ReadAsStringAsync();
            if (string.IsNullOrWhiteSpace(body))
            {
                return null;
            }

            using var doc = JsonDocument.Parse(body);
            return doc.RootElement.Clone();
        }

        private static void AddThirdDifferenceWarning(
            List<string> warnings,
            string fieldName,
            string? complaintValue,
            string? gestionaValue,
            bool normalizeAsCode = false)
        {
            if (string.IsNullOrWhiteSpace(complaintValue))
            {
                return;
            }

            var left = normalizeAsCode
                ? NormalizeKey(complaintValue)
                : NormalizeForComparison(complaintValue);
            var right = normalizeAsCode
                ? NormalizeKey(gestionaValue)
                : NormalizeForComparison(gestionaValue);

            if (string.Equals(left, right, StringComparison.Ordinal))
            {
                return;
            }

            warnings.Add(
                $"El campo {fieldName} no coincide con Gestiona (denuncia: '{FormatWarningValue(complaintValue)}'; Gestiona: '{FormatWarningValue(gestionaValue)}').");
        }

        private static string NormalizeForComparison(string? value)
        {
            return string.Join(
                ' ',
                (value ?? string.Empty)
                    .Trim()
                    .Split(' ', StringSplitOptions.RemoveEmptyEntries))
                .ToUpperInvariant();
        }

        private static string FormatWarningValue(string? value)
        {
            var normalized = string.Join(
                ' ',
                (value ?? string.Empty)
                    .Trim()
                    .Split(' ', StringSplitOptions.RemoveEmptyEntries));
            if (string.IsNullOrWhiteSpace(normalized))
            {
                return "sin dato";
            }

            const int maxLength = 80;
            return normalized.Length <= maxLength
                ? normalized
                : normalized[..maxLength] + "...";
        }

        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(),
                    NotificationPreference = thirdParty.NotificationPreference ?? string.Empty,
                    ElectronicNotification = thirdParty.ElectronicNotification ?? string.Empty,
                    PostalNotificationPreference = thirdParty.PostalNotificationPreference ?? string.Empty,
                    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,
                NotificationPreference = thirdParty.NotificationPreference ?? string.Empty,
                ElectronicNotification = thirdParty.ElectronicNotification ?? string.Empty,
                PostalNotificationPreference = thirdParty.PostalNotificationPreference ?? string.Empty,
                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?> BuscarExpedientePorCodigoAsync(string codigoExpediente)
        {
            if (string.IsNullOrWhiteSpace(codigoExpediente))
            {
                return null;
            }

            var normalizedCode = codigoExpediente.Trim();
            var json = await GetFilesAsync(new
            {
                code = normalizedCode
            });
            using var doc = JsonDocument.Parse(json);

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

            GestionaExpedienteInfo? first = null;
            foreach (var item in content.EnumerateArray())
            {
                var expediente = BuildExpedienteInfo(item);
                if (expediente is null)
                {
                    continue;
                }

                if (first is null)
                {
                    first = expediente;
                }

                if (string.Equals(
                    expediente.CodigoExpediente?.Trim(),
                    normalizedCode,
                    StringComparison.OrdinalIgnoreCase))
                {
                    return expediente;
                }
            }

            return first;
        }

        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<GestionaAuditInfo?> ObtenerUltimaAuditoriaExpedienteAsync(string fileUrl)
        {
            if (string.IsNullOrWhiteSpace(fileUrl))
            {
                return null;
            }

            var auditUrl = $"{fileUrl.TrimEnd('/')}/audit";
            using var req = new HttpRequestMessage(HttpMethod.Get, auditUrl);
            req.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", _opts.AccessToken);
            req.Headers.Accept.Clear();
            req.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/vnd.gestiona.audit.entries-page"));

            using var resp = await _http.SendAsync(req);
            LogDeprecatedHeaders(resp, $"GET {auditUrl}");
            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(
                    $"ObtenerUltimaAuditoriaExpedienteAsync: {(int)resp.StatusCode} {resp.ReasonPhrase}\n{body}");
            }

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

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

            GestionaAuditInfo? latest = null;
            foreach (var item in content.EnumerateArray())
            {
                var entryDate = GetJsonDateTimeOffset(item, "entry_date");
                if (entryDate is null)
                {
                    continue;
                }

                if (latest?.Fecha is not null && entryDate.Value <= latest.Fecha.Value)
                {
                    continue;
                }

                latest = new GestionaAuditInfo
                {
                    Fecha = entryDate,
                    Mensaje = GetJsonString(item, "message")
                };
            }

            return latest;
        }

        public async Task<List<ExpedienteTerceroDto>> ObtenerExpedientesPorTerceroAsync(
            string nif,
            DateTimeOffset? desde = null,
            DateTimeOffset? hasta = null,
            int maxPages = 1,
            int maxResults = 30,
            int maxParallel = 6
        )
        {
            if (string.IsNullOrWhiteSpace(nif))
                throw new ArgumentException("NIF obligatorio.", nameof(nif));

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

            var resultados = new List<ExpedienteTerceroDto>();
            var json = await GetFilesAsync(new
            {
                dni = nif
            });
            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;
                }

                var creation = GetJsonDateTimeOffset(item, "creation_date");
                var updated = GetFirstJsonDateTimeOffset(
                    item,
                    "update_date",
                    "updated_at",
                    "modification_date",
                    "modified_at",
                    "last_update",
                    "last_modified");

                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,
                    Procedimiento = procedureName,
                    FechaCreacion = creation,
                    FechaUltimaModificacion = updated ?? creation,
                    Estado = state
                });
            }

            await EnrichExpedientesWithAuditAsync(resultados, maxParallel);
            return resultados;
        }

        private async Task EnrichExpedientesWithAuditAsync(
            IReadOnlyList<ExpedienteTerceroDto> expedientes,
            int maxParallel)
        {
            if (expedientes.Count == 0)
            {
                return;
            }

            var parallelism = Math.Clamp(maxParallel, 1, 6);
            using var gate = new SemaphoreSlim(parallelism, parallelism);
            var tasks = expedientes
                .Where(expediente => !string.IsNullOrWhiteSpace(expediente.FileUrl))
                .Select(async expediente =>
                {
                    await gate.WaitAsync();
                    try
                    {
                        var audit = await ObtenerUltimaAuditoriaExpedienteAsync(expediente.FileUrl);
                        if (audit is null)
                        {
                            return;
                        }

                        if (audit.Fecha is not null)
                        {
                            expediente.FechaUltimaModificacion = audit.Fecha;
                        }

                        if (!string.IsNullOrWhiteSpace(audit.Mensaje))
                        {
                            expediente.UltimaAuditoriaMensaje = audit.Mensaje;
                        }
                    }
                    catch (Exception ex)
                    {
                        _logger.LogWarning(
                            ex,
                            "No se ha podido consultar la auditoria del expediente {FileUrl}.",
                            expediente.FileUrl);
                    }
                    finally
                    {
                        gate.Release();
                    }
                });

            await Task.WhenAll(tasks);
        }

        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 DateTimeOffset? GetFirstJsonDateTimeOffset(JsonElement item, params string[] propertyNames)
        {
            foreach (var propertyName in propertyNames)
            {
                var value = GetJsonDateTimeOffset(item, propertyName);
                if (value is not null)
                {
                    return value;
                }
            }

            return null;
        }

        private static DateTimeOffset? GetJsonDateTimeOffset(JsonElement item, string propertyName)
        {
            if (!item.TryGetProperty(propertyName, out var property))
            {
                return null;
            }

            if (property.ValueKind == JsonValueKind.Number &&
                property.TryGetInt64(out var timestamp))
            {
                return DateTimeOffset.FromUnixTimeSeconds(timestamp);
            }

            if (property.ValueKind != JsonValueKind.String)
            {
                return null;
            }

            var raw = property.GetString();
            if (string.IsNullOrWhiteSpace(raw))
            {
                return null;
            }

            if (long.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out var timestampString))
            {
                return DateTimeOffset.FromUnixTimeSeconds(timestampString);
            }

            return DateTimeOffset.TryParse(
                raw.Replace("Z", "+00:00", StringComparison.Ordinal),
                CultureInfo.InvariantCulture,
                DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
                out var parsed)
                ? parsed
                : 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)
        {
            var notificationText = NormalizeKey(string.Join(
                ' ',
                thirdParty.NotificationPreference,
                thirdParty.ElectronicNotification,
                thirdParty.PostalNotificationPreference));

            var requestsPostal =
                notificationText.Contains("CORREO POSTAL", StringComparison.Ordinal) ||
                notificationText.Contains("POSTAL", StringComparison.Ordinal);
            var requestsElectronic =
                notificationText.Contains("ELECTRONICA", StringComparison.Ordinal) ||
                notificationText.Contains("TELEMATICA", StringComparison.Ordinal) ||
                notificationText.Contains("ONLINE", StringComparison.Ordinal);

            if (requestsPostal && thirdParty.Address?.HasAnyValue == true && !requestsElectronic)
            {
                return "PAPER";
            }

            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));
        }



    }
}
