using System.Text.RegularExpressions;
using JobsMedical.Web.Models;

namespace JobsMedical.Web.Services.Scraping;

/// <summary>
/// Scrapes clinical job ads from iranestekhdam.ir. It reads the site's monthly ad sitemaps
/// (sitemap-ads.xml → sitemap-ads-YYYY-M.xml) to enumerate ad URLs, keeps only those whose
/// readable Persian slug names a CLINICAL role (veterinary / non-clinical excluded), then fetches
/// each ad page and extracts its title + description (+ any phone). These are EMPLOYER ads at NAMED
/// facilities (بیمارستان/درمانگاه/کلینیک/آزمایشگاه …) — far higher quality than classifieds, so they
/// directly improve the «نامشخص»-facility problem. Content-hash dedupe ingests each ad once; the
/// medical-gate validator + AI auditor + junk filters do the final screening on top.
/// </summary>
public class IranEstekhdamListingSource : IListingSource
{
    private const string SitemapIndex = "https://iranestekhdam.ir/sitemap-ads.xml";
    private readonly ScrapeHttpClients _clients;
    private readonly ILogger<IranEstekhdamListingSource> _log;

    public IranEstekhdamListingSource(ScrapeHttpClients clients, ILogger<IranEstekhdamListingSource> log)
    {
        _clients = clients;
        _log = log;
    }

    public string Name => "ایران‌استخدام (iranestekhdam.ir)";

    // Clinical-role markers matched against the DECODED Persian URL slug. Words are hyphen-joined in
    // the slug, so substring matching works on the decoded form.
    private static readonly string[] RoleSlugs =
    {
        "پرستار", "بهیار", "کمک-پرستار", "کمک-بهیار", "پزشک", "دندان", "مامایی", "ماما", "تکنسین",
        "رادیولوژ", "سونوگراف", "فیزیوتراپ", "کاردرمان", "گفتاردرمان", "شنوایی", "بینایی", "اپتومتر",
        "دیالیز", "اتاق-عمل", "بیهوش", "تزریقات", "فوریت", "اورژانس", "داروساز", "نسخه", "سالمند",
    };

    // Slugs that share a substring with a clinical role but are NOT کادر درمان — drop them.
    private static readonly string[] ExcludeSlugs = { "دامپزشک", "دام-پزشک", "دامپزشکی" };

    // LAUNCH = TEHRAN ONLY. We keep only ads located in Tehran (the ad's og:description reliably
    // states «شهر تهران»). Other major cities named in the slug are pre-dropped to save fetches.
    // When the engine is proven and we expand nationwide, make this a per-source city setting.
    private const string Tehran = "تهران";
    private static readonly string[] OtherCitySlugs =
    {
        "شیراز", "اصفهان", "مشهد", "تبریز", "کرج", "اهواز", "قم", "یزد", "رشت", "کرمان", "اراک",
        "اردبیل", "همدان", "کرمانشاه", "زنجان", "قزوین", "ساری", "گرگان", "بندرعباس", "بوشهر",
        "سنندج", "خرم-آباد", "بیرجند", "سمنان", "شهرکرد", "ایلام", "یاسوج", "زاهدان", "ارومیه",
        "نجف-آباد", "کاشان", "قائم-شهر", "بابل", "آمل", "دزفول", "ملارد", "پاکدشت",
    };

    public async Task<IReadOnlyList<ScrapedItem>> FetchAsync(AppSetting s, CancellationToken ct = default)
    {
        if (!s.IranEstekhdamEnabled) return Array.Empty<ScrapedItem>();
        var max = Math.Clamp(s.IranEstekhdamMaxAds, 1, 500);
        var client = _clients.For(s, s.IranEstekhdamUseProxy);

        try
        {
            // 1. sitemap index → the monthly ad sitemaps (newest first as listed by the site)
            var index = await client.GetStringAsync(SitemapIndex, ct);
            var monthly = Locs(index).Where(u => u.Contains("sitemap-ads-")).ToList();
            if (monthly.Count == 0) { _log.LogWarning("iranestekhdam: no monthly ad sitemaps found"); return Array.Empty<ScrapedItem>(); }

            // 2. pool clinical-role candidate URLs, pre-dropping obvious non-Tehran slugs. We gather
            //    more than `max` because the authoritative Tehran check (on the ad text) trims further.
            var pool = new List<string>();
            var budget = max * 5;
            foreach (var sm in monthly)
            {
                if (pool.Count >= budget) break;
                try
                {
                    foreach (var u in Locs(await client.GetStringAsync(sm, ct)))
                    {
                        if (IsClinicalSlug(u) && !IsOtherCitySlug(u) && !pool.Contains(u)) pool.Add(u);
                        if (pool.Count >= budget) break;
                    }
                }
                catch (Exception ex) { _log.LogWarning(ex, "iranestekhdam: sitemap {Sm} failed", sm); }
            }

            // 3. fetch each ad → keep only Tehran ones (text must name «تهران»), up to `max`.
            var items = new List<ScrapedItem>();
            foreach (var url in pool)
            {
                if (items.Count >= max) break;
                ct.ThrowIfCancellationRequested();
                try
                {
                    var html = await client.GetStringAsync(url, ct);
                    var text = ExtractAd(html);
                    if (text.Length < 25 || !text.Contains(Tehran)) continue;   // Tehran-only launch filter
                    items.Add(new ScrapedItem("ایران‌استخدام", text, url));
                }
                catch (Exception ex) { _log.LogWarning(ex, "iranestekhdam: ad {Url} failed", url); }
            }
            _log.LogInformation("iranestekhdam: fetched {Count} Tehran clinical ads (from {Pool} pooled)", items.Count, pool.Count);
            return items;
        }
        catch (Exception ex)
        {
            _log.LogWarning(ex, "iranestekhdam fetch failed");
            return Array.Empty<ScrapedItem>();
        }
    }

    private static bool IsClinicalSlug(string url)
    {
        var slug = Uri.UnescapeDataString(url);
        if (ExcludeSlugs.Any(slug.Contains)) return false;
        return RoleSlugs.Any(slug.Contains);
    }

    private static bool IsOtherCitySlug(string url)
    {
        var slug = Uri.UnescapeDataString(url);
        return OtherCitySlugs.Any(slug.Contains);
    }

    private static IEnumerable<string> Locs(string xml)
        => Regex.Matches(xml, "<loc>([^<]+)</loc>").Select(m => m.Groups[1].Value.Trim());

    /// <summary>Title (site suffix stripped) + the ad's description. iranestekhdam puts a complete,
    /// structured summary (facility + city + district + role) in og:description, with the full
    /// requirements in the .single-ad container — prefer whichever yields more text.</summary>
    private static string ExtractAd(string html)
    {
        var title = Meta(html, "og:title");
        if (title is not null) { var bar = title.IndexOf('|'); if (bar > 10) title = title[..bar].Trim(); }

        var ogBody = Meta(html, "og:description");
        var single = BetweenClass(html, "single-ad");
        var singleText = single is null ? null : HtmlUtil.ToPlainText(single);
        var body = (singleText?.Length ?? 0) > (ogBody?.Length ?? 0) ? singleText : ogBody;

        var text = HtmlUtil.ToPlainText(string.Join("\n", new[] { title, body }.Where(p => !string.IsNullOrWhiteSpace(p))));
        if (text.Length > 1800) text = text[..1800];

        var phones = HtmlUtil.HarvestPhones(body ?? "");
        if (phones.Count > 0 && !phones.Any(text.Contains))
            text += "\nشماره تماس: " + string.Join("، ", phones);
        return text;
    }

    private static string? Meta(string html, string prop)
    {
        var m = Regex.Match(html, $"<meta[^>]+property=[\"']{Regex.Escape(prop)}[\"'][^>]+content=[\"']([^\"']*)[\"']");
        return m.Success ? System.Net.WebUtility.HtmlDecode(m.Groups[1].Value) : null;
    }

    private static string? BetweenClass(string html, string cls)
    {
        var m = Regex.Match(html, $"<(?:div|article|section)[^>]+class=[\"'][^\"']*{Regex.Escape(cls)}[^\"']*[\"'][^>]*>(.*?)</(?:div|article|section)>",
            RegexOptions.Singleline);
        return m.Success ? m.Groups[1].Value : null;
    }
}
