using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using ytLive.Models;

namespace ytLive.Services;

public sealed class SocialLookupResult
{
    public bool Success { get; init; }
    public string ProfileUrl { get; init; } = string.Empty;
    public string Handle { get; init; } = string.Empty;
    public string Error { get; init; } = string.Empty;

    /// <summary>Fediverse instance software (nodeinfo `software.name`), when the handle is federated.</summary>
    public string? FediverseSoftware { get; init; }

    /// <summary>True when the caller canceled the lookup before it finished.</summary>
    public bool Canceled { get; init; }
}

public interface ISocialValidator
{
    Task<SocialLookupResult> LookupAsync(SocialService service, string handleOrUrl, CancellationToken ct);

    /// <summary>Best-effort nodeinfo software name (mastodon/peertube/...) for a
    /// fediverse domain, or null when it can't be resolved. The load-time heal
    /// uses this to fix a bar entry whose stored software is missing.</summary>
    Task<string?> ResolveFediverseSoftwareAsync(string domain, CancellationToken ct);
}

/// <summary>
/// Validates a social handle/URL by constructing the canonical profile URL and
/// issuing an HTTP GET — the "act of validation" the creator trusts before the
/// icon lands on their bar. Best-effort: some platforms return challenges/blocks
/// to HEAD requests, so a 200 OR a 301/302 redirect counts as "exists"; 404 or
/// connection failure = rejected. Never blind trust.
/// </summary>
public sealed class HttpSocialValidator : ISocialValidator
{
    private readonly HttpClient _client;
    private readonly Action<string>? _log;

    public HttpSocialValidator(HttpClient? client = null, Action<string>? log = null)
    {
        _client = client ?? new HttpClient();
        _client.DefaultRequestHeaders.UserAgent.ParseAdd(
            "Mozilla/5.0 (compatible; ytLlive social validator)");
        _log = log;
    }

    public Task<string?> ResolveFediverseSoftwareAsync(string domain, CancellationToken ct)
        => TryFetchFediverseSoftwareAsync(domain, ct);

    public async Task<SocialLookupResult> LookupAsync(SocialService service, string handleOrUrl, CancellationToken ct)
    {
        var input = handleOrUrl.Trim();
        if (string.IsNullOrWhiteSpace(input))
            return new SocialLookupResult { Error = "Enter a handle or URL." };

        // Fediverse handle (@user@domain): the domain is part of the identity,
        // so the canonical URL can't be rebuilt from a bare handle. After the
        // profile validates, ask the instance which software it runs (nodeinfo)
        // so the caller can show the right service logo.
        if (input.StartsWith('@') && input.IndexOf('@', 1) > 0)
        {
            var at = input.IndexOf('@', 1);
            var user = input[1..at];
            var domain = input[(at + 1)..];
            if (user.Length > 0 && domain.Length > 0)
            {
                var url = $"https://{domain}/@{user}";
                var result = await CheckAsync(service, input, url, ct);
                if (!result.Success) return result;
                var software = await TryFetchFediverseSoftwareAsync(domain, ct);
                if (ct.IsCancellationRequested)
                    return new SocialLookupResult { Canceled = true };
                return new SocialLookupResult
                {
                    Success = true,
                    ProfileUrl = result.ProfileUrl,
                    Handle = result.Handle,
                    FediverseSoftware = software,
                };
            }
        }

        var handle = input;
        if (input.StartsWith("http://", System.StringComparison.OrdinalIgnoreCase) ||
            input.StartsWith("https://", System.StringComparison.OrdinalIgnoreCase))
        {
            var uri = new System.Uri(input);
            handle = uri.AbsolutePath.Trim('/');
        }

        var canonical = SocialServiceIcons.CanonicalUrlFor(service, handle);
        return await CheckAsync(service, handle, canonical, ct);
    }

    private async Task<SocialLookupResult> CheckAsync(SocialService service, string handle, string url, CancellationToken ct)
    {
        try
        {
            using var resp = await _client.GetAsync(url, ct);
            if (resp.IsSuccessStatusCode || (int)resp.StatusCode is 301 or 302 or 303 or 307 or 308)
                return new SocialLookupResult
                {
                    Success = true,
                    ProfileUrl = url,
                    Handle = handle,
                };
            return new SocialLookupResult
            {
                Error = $"{service} returned {resp.StatusCode} for '{handle}'. The handle may be wrong or the page doesn't exist.",
            };
        }
        catch (System.OperationCanceledException) when (ct.IsCancellationRequested)
        {
            return new SocialLookupResult { Canceled = true };
        }
        catch (System.Net.Http.HttpRequestException)
        {
            return new SocialLookupResult
            {
                Error = $"Couldn't reach {service} to validate '{handle}'. Check your connection and try again.",
            };
        }
        catch (System.Exception ex)
        {
            return new SocialLookupResult { Error = ex.Message };
        }
    }

    /// <summary>
    /// Best-effort nodeinfo lookup: GET /.well-known/nodeinfo, follow the first
    /// nodeinfo link, read `software.name`. If the identity domain is itself a
    /// redirect (e.g. a YunoHost domain whose default app lives on a subdomain),
    /// the bare root 302s to the real instance — follow it and ask that host.
    /// If neither answers, probe well-known subdomains (mastodon.example.com)
    /// so a landing-page domain still resolves its real instance. The whole
    /// resolution is bounded by a ~15s budget. Failures return null — validation
    /// still succeeds, the icon just falls back to the generic fediverse glyph.
    /// </summary>
    private async Task<string?> TryFetchFediverseSoftwareAsync(string domain, CancellationToken ct)
    {
        try
        {
            using var budget = System.Threading.CancellationTokenSource.CreateLinkedTokenSource(ct);
            budget.CancelAfter(TimeSpan.FromSeconds(15));

            var software = await FetchSoftwareNameAsync(domain, budget.Token);
            if (software != null) return software;

            var resolved = await ResolveInstanceHostAsync(domain, budget.Token);
            if (resolved != null
                && !string.Equals(resolved, domain, System.StringComparison.OrdinalIgnoreCase))
            {
                software = await FetchSoftwareNameAsync(resolved, budget.Token);
                if (software != null) return software;
            }

            foreach (var sub in FediverseSubdomainCandidates)
            {
                var host = $"{sub}.{domain}";
                software = await FetchSoftwareNameAsync(host, budget.Token);
                if (software != null)
                {
                    _log?.Invoke($"SocialValidator: fediverse software '{software}' for '{domain}' resolved via {host}");
                    return software;
                }
            }
            _log?.Invoke($"SocialValidator: no nodeinfo found for '{domain}'");
            return null;
        }
        catch (System.OperationCanceledException) when (ct.IsCancellationRequested)
        {
            return null; // caller checks ct.IsCancellationRequested and reports Canceled
        }
        catch (System.OperationCanceledException)
        {
            _log?.Invoke($"SocialValidator: nodeinfo resolution for '{domain}' timed out");
            return null;
        }
        catch (System.Exception ex)
        {
            _log?.Invoke($"SocialValidator: nodeinfo resolution for '{domain}' failed: {ex.Message}");
            return null;
        }
    }

    /// <summary>Common instance subdomains, probed in order when the identity
    /// domain itself doesn't serve nodeinfo (a landing page or SSO gate in front
    /// of a self-hosted instance).</summary>
    private static readonly string[] FediverseSubdomainCandidates =
    {
        "mastodon", "social", "fediverse", "tube", "peertube",
        "pixelfed", "lemmy", "pleroma", "misskey", "gotosocial", "fedi", "instance",
    };

    private async Task<string?> FetchSoftwareNameAsync(string domain, CancellationToken ct)
    {
        var nodeInfoUrl = await FetchNodeInfoUrlAsync(domain, ct);
        if (nodeInfoUrl == null) return null;
        using var resp = await _client.GetAsync(nodeInfoUrl, ct);
        if (!resp.IsSuccessStatusCode) return null;
        var json = await resp.Content.ReadAsStringAsync(ct);
        using var doc = JsonDocument.Parse(json);
        if (doc.RootElement.TryGetProperty("software", out var software)
            && software.TryGetProperty("name", out var name))
            return name.GetString();
        return null;
    }

    /// <summary>Follows the bare-domain root redirect (HttpClient follows 3xx automatically)
    /// and returns the final host — the "default app" behind an identity domain.</summary>
    private async Task<string?> ResolveInstanceHostAsync(string domain, CancellationToken ct)
    {
        using var resp = await _client.GetAsync($"https://{domain}/", ct);
        return resp.RequestMessage?.RequestUri?.Host;
    }

    private async Task<string?> FetchNodeInfoUrlAsync(string domain, CancellationToken ct)
    {
        using var resp = await _client.GetAsync($"https://{domain}/.well-known/nodeinfo", ct);
        if (!resp.IsSuccessStatusCode) return null;
        var json = await resp.Content.ReadAsStringAsync(ct);
        using var doc = JsonDocument.Parse(json);
        if (!doc.RootElement.TryGetProperty("links", out var links)) return null;
        foreach (var link in links.EnumerateArray())
        {
            if (!link.TryGetProperty("rel", out var rel)) continue;
            var relStr = rel.GetString() ?? string.Empty;
            if (!relStr.Contains("nodeinfo", System.StringComparison.OrdinalIgnoreCase)) continue;
            if (!link.TryGetProperty("href", out var href) || href.GetString() is not { } hrefStr) continue;
            if (System.Uri.TryCreate(hrefStr, System.UriKind.RelativeOrAbsolute, out var parsed))
                return parsed.IsAbsoluteUri
                    ? parsed.AbsoluteUri
                    : new System.Uri(new System.Uri($"https://{domain}"), parsed).AbsoluteUri;
        }
        return null;
    }
}
