//! [registries.io](https://registries.io/) company search API (`/api/companies`) for PL, CZ, etc.
//! Uses browser-like headers; paginates via `meta.totalPages`.

use std::time::Duration;

use crate::audit::{logged_send, AuditBus, HttpAuditMeta};
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
use serde::Deserialize;
use serde_json::{json, Value};
use tokio::time::sleep;
use url::Url;

pub const DEFAULT_UA: &str = concat!(
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ",
    "(KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"
);

const MAX_RETRIES: u32 = 4;
const PAGE_DELAY_MS: u64 = 250;

/// One company object from `data[]` (camelCase API).
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RegistriesIoCompany {
    #[serde(default)]
    pub name: String,
    #[serde(default)]
    pub slug: Option<String>,
    #[serde(default)]
    pub registration_number: Option<String>,
    #[serde(default)]
    pub registration_numbers: Option<Vec<String>>,
    #[serde(default)]
    pub country: String,
    #[serde(default)]
    pub legal_form: Option<String>,
    #[serde(default)]
    pub registration_date: Option<String>,
    #[serde(default)]
    pub status: Option<String>,
    #[serde(default)]
    pub address: Option<String>,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct CompaniesResponse {
    #[serde(default)]
    data: Vec<RegistriesIoCompany>,
    #[serde(default)]
    meta: ResponseMeta,
}

#[derive(Debug, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ResponseMeta {
    #[serde(default)]
    total_pages: u32,
}

pub(crate) fn primary_registration_id(c: &RegistriesIoCompany) -> Option<String> {
    let from_field = c
        .registration_number
        .as_ref()
        .map(|s| s.trim().to_string())
        .filter(|s| !s.is_empty());
    if from_field.is_some() {
        return from_field;
    }
    c.registration_numbers
        .as_ref()
        .and_then(|v| v.first())
        .map(|s| s.trim().to_string())
        .filter(|s| !s.is_empty())
}

fn referer_for_search(query: &str, country_lower: &str) -> String {
    let q = utf8_percent_encode(query, NON_ALPHANUMERIC);
    format!("https://registries.io/search?query={q}&country={country_lower}")
}

/// Fetch all pages for `filter.query` + `filter.country` (ISO2 uppercase).
pub async fn fetch_all_registries_io_companies(
    client: &reqwest::Client,
    base_url: &str,
    query: &str,
    country_code: &str,
    page_limit: u32,
    audit: Option<&AuditBus>,
    http_audit_profile: Option<String>,
) -> Vec<RegistriesIoCompany> {
    let root = base_url.trim_end_matches('/');
    let q = query.trim();
    if q.is_empty() {
        return vec![];
    }
    let cc = country_code.trim().to_uppercase();
    if cc.len() != 2 {
        return vec![];
    }
    let cc_lower = cc.to_lowercase();
    let referer = referer_for_search(q, &cc_lower);

    let mut delay_secs = 1.0_f64;
    let first = match fetch_registries_io_page(
        client,
        root,
        q,
        &cc,
        1,
        page_limit,
        &referer,
        audit,
        http_audit_profile.clone(),
        &mut delay_secs,
    )
    .await
    {
        Ok(resp) => resp,
        Err(e) => {
            tracing::warn!("registries.io: {}", e);
            return vec![];
        }
    };

    let total_pages = if first.meta.total_pages > 0 {
        first.meta.total_pages
    } else if first.data.is_empty() {
        0
    } else {
        1
    };

    let mut out: Vec<RegistriesIoCompany> = first.data;
    let mut seen = std::collections::HashSet::new();
    for c in &out {
        if let Some(id) = primary_registration_id(c) {
            seen.insert(id);
        }
    }

    for page in 2..=total_pages {
        sleep(Duration::from_millis(PAGE_DELAY_MS)).await;
        let res = match fetch_registries_io_page(
            client,
            root,
            q,
            &cc,
            page,
            page_limit,
            &referer,
            audit,
            http_audit_profile.clone(),
            &mut delay_secs,
        )
        .await
        {
            Ok(r) => r,
            Err(e) => {
                tracing::warn!("registries.io page {}: {}", page, e);
                break;
            }
        };
        let rows = res.data;
        if rows.is_empty() {
            break;
        }
        for c in rows {
            if let Some(id) = primary_registration_id(&c) {
                if seen.insert(id.clone()) {
                    out.push(c);
                }
            } else {
                out.push(c);
            }
        }
    }

    out
}

async fn fetch_registries_io_page(
    client: &reqwest::Client,
    root: &str,
    query: &str,
    country_upper: &str,
    page: u32,
    limit: u32,
    referer: &str,
    audit: Option<&AuditBus>,
    http_audit_profile: Option<String>,
    delay_secs: &mut f64,
) -> Result<CompaniesResponse, String> {
    let api_path = format!("{root}/api/companies");
    let mut url = Url::parse(&api_path).map_err(|e| e.to_string())?;
    url.query_pairs_mut()
        .append_pair("filter.query", query)
        .append_pair("filter.country", country_upper)
        .append_pair("page", &page.to_string())
        .append_pair("limit", &limit.to_string());

    let url_s = url.to_string();
    let mut last_err = String::new();

    for attempt in 0..MAX_RETRIES {
        let req = client
            .get(url_s.clone())
            .header("accept", "application/json, text/plain, */*")
            .header("accept-language", "en-US,en;q=0.9")
            .header("referer", referer)
            .header("user-agent", DEFAULT_UA);

        let meta = HttpAuditMeta {
            source: "registries_io",
            profile_name: http_audit_profile.clone(),
            config_name: None,
            extra: Some(json!({
                "country": country_upper,
                "page": page,
            })),
        };

        match logged_send(audit, req.send(), "GET", &url_s, meta).await {
            Ok(resp) => {
                let status = resp.status();
                if status.is_success() {
                    return resp
                        .json::<CompaniesResponse>()
                        .await
                        .map_err(|e| e.to_string());
                }
                if status.as_u16() == 429 || status.as_u16() == 503 {
                    last_err = format!("HTTP {}", status);
                    tracing::warn!(
                        "registries.io {}: retry in {:.0}s",
                        status,
                        *delay_secs
                    );
                    sleep(Duration::from_secs_f64(*delay_secs)).await;
                    *delay_secs = (*delay_secs * 2.0).min(30.0);
                    continue;
                }
                let text = resp.text().await.unwrap_or_default();
                return Err(format!(
                    "{}: {}",
                    status,
                    text.chars().take(200).collect::<String>()
                ));
            }
            Err(e) => {
                last_err = e.to_string();
                tracing::warn!(
                    "registries.io request error (attempt {}): {}",
                    attempt + 1,
                    e
                );
                sleep(Duration::from_secs_f64(*delay_secs)).await;
                *delay_secs = (*delay_secs * 2.0).min(30.0);
            }
        }
    }

    Err(last_err)
}

fn referer_for_company_page(country_lower: &str, slug: &str) -> String {
    format!(
        "https://registries.io/{}/{}",
        country_lower,
        slug.trim()
    )
}

/// Full company profile (`sections` array with BASIC_INFO, ADDRESSES, …).  
/// GET `{base}/api/companies/{slug}` (JSON).
pub async fn fetch_registries_io_company_detail(
    client: &reqwest::Client,
    root: &str,
    slug: &str,
    country_lower: &str,
    audit: Option<&AuditBus>,
    http_audit_profile: Option<String>,
) -> Option<Value> {
    let slug = slug.trim();
    if slug.is_empty() {
        return None;
    }
    let path_slug = if slug
        .bytes()
        .all(|b| matches!(b, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'-' | b'_'))
    {
        slug.to_string()
    } else {
        utf8_percent_encode(slug, NON_ALPHANUMERIC).to_string()
    };
    let url_s = format!("{root}/api/companies/{path_slug}");
    let referer = referer_for_company_page(country_lower, slug);
    let mut delay_secs = 1.0_f64;
    let mut last_err = String::new();

    for attempt in 0..MAX_RETRIES {
        let req = client
            .get(&url_s)
            .header("accept", "application/json, text/plain, */*")
            .header("accept-language", "en-US,en;q=0.9")
            .header("referer", &referer)
            .header("user-agent", DEFAULT_UA);

        let meta = HttpAuditMeta {
            source: "registries_io_detail",
            profile_name: http_audit_profile.clone(),
            config_name: None,
            extra: Some(json!({ "slug": slug })),
        };

        match logged_send(audit, req.send(), "GET", &url_s, meta).await {
            Ok(resp) => {
                let status = resp.status();
                if status.is_success() {
                    let Ok(val) = resp.json::<Value>().await else {
                        tracing::warn!("registries.io detail: invalid JSON for slug={}", slug);
                        return None;
                    };
                    // Some gateways wrap the entity; unwrap common shapes.
                    let inner = if let Some(d) = val.get("data") {
                        d.clone()
                    } else if let Some(d) = val.get("company") {
                        d.clone()
                    } else {
                        val
                    };
                    if inner.get("sections").is_some() || inner.get("name").is_some() {
                        return Some(inner);
                    }
                    tracing::warn!("registries.io detail: unexpected JSON shape for slug={}", slug);
                    return None;
                }
                if status.as_u16() == 429 || status.as_u16() == 503 {
                    last_err = format!("HTTP {}", status);
                    tracing::warn!(
                        "registries.io detail {}: retry in {:.0}s",
                        status,
                        delay_secs
                    );
                    sleep(Duration::from_secs_f64(delay_secs)).await;
                    delay_secs = (delay_secs * 2.0).min(30.0);
                    continue;
                }
                let text = resp.text().await.unwrap_or_default();
                tracing::warn!(
                    "registries.io detail {}: {}",
                    status,
                    text.chars().take(200).collect::<String>()
                );
                return None;
            }
            Err(e) => {
                last_err = e.to_string();
                tracing::warn!(
                    "registries.io detail request error (attempt {}): {}",
                    attempt + 1,
                    e
                );
                sleep(Duration::from_secs_f64(delay_secs)).await;
                delay_secs = (delay_secs * 2.0).min(30.0);
            }
        }
    }
    tracing::warn!("registries.io detail failed after retries: {}", last_err);
    None
}
