import { normalizePhone, normalizeUrl } from "../quality.js";
import { normalizeText, uniqueBy } from "../utils.js";

const OWNER_TERMS = [
  "owner",
  "founder",
  "co-founder",
  "cofounder",
  "co-owner",
  "coowner",
  "ceo",
  "president",
  "principal",
  "proprietor",
  "managing partner",
  "operator",
  "founded",
  "owned"
];

const SOCIAL_RE = /(?:^|:\/\/|[/.])(linkedin\.com|facebook\.com|instagram\.com|twitter\.com|x\.com|tiktok\.com)(?:[/:?#]|$)/i;
const LINKEDIN_PROFILE_RE = /(?:^|:\/\/|[/.])linkedin\.com\/in\//i;
const LINKEDIN_COMPANY_RE = /(?:^|:\/\/|[/.])linkedin\.com\/company\//i;
const PUBLIC_EVIDENCE_RE = /(?:^|:\/\/|[/.])(linkedin\.com|facebook\.com|instagram\.com|twitter\.com|x\.com|tiktok\.com|crunchbase\.com|alignable\.com|about\.me|linktr\.ee|beacons\.ai|medium\.com|substack\.com|youtube\.com|bizjournals\.com|patch\.com|chamberofcommerce\.com|bbb\.org|opencorporates\.com|bizapedia\.com|corporationwiki\.com|dnb\.com|manta\.com|buzzfile\.com|texas-corp\.com|mycpa\.cpa\.state\.tx\.us)(?:[/:?#]|$)|(?:^|:\/\/|[/.])(voyage|canvasrebel)[a-z0-9.-]*(?:[/:?#]|$)/i;
const MAX_SEARCH_QUERIES = 12;
const MAX_SEARCH_RESULTS_PER_QUERY = 4;
const MAX_OWNER_AGENTS = 50;
const SEED_EMAIL_PROBE_QUERY_LIMIT = 4;
const OWNER_AGENT_TRACE_LIMIT = 100;
const OWNER_AGENT_EXECUTION_CONCURRENCY = Math.max(1, Math.min(12, Number(process.env.HERMES_OWNER_AGENT_EXECUTION_CONCURRENCY || 6)));
const OWNER_SEARCH_QUERY_CONCURRENCY = Math.max(1, Math.min(8, Number(process.env.HERMES_OWNER_SEARCH_QUERY_CONCURRENCY || 4)));
const DUCKDUCKGO_TIMEOUT_MS = Math.max(1000, Math.min(10000, Number(process.env.HERMES_DUCKDUCKGO_TIMEOUT_MS || 2500)));
const BING_TIMEOUT_MS = Math.max(2000, Math.min(15000, Number(process.env.HERMES_BING_TIMEOUT_MS || 8000)));
const OWNER_MINIMAX_PLANNING_MIN_TIMEOUT_MS = Math.max(10000, Number(process.env.HERMES_OWNER_MINIMAX_PLANNING_MIN_TIMEOUT_MS || 60000));
const OWNER_MINIMAX_PLANNING_TIMEOUT_MS = Math.max(3000, Math.min(30000, Number(process.env.HERMES_OWNER_MINIMAX_PLANNING_TIMEOUT_MS || 12000)));
const OWNER_MINIMAX_TIMEOUT_COUNT_CIRCUIT = Math.max(1, Number(process.env.HERMES_OWNER_MINIMAX_TIMEOUT_COUNT_CIRCUIT || 5));
const OWNER_MINIMAX_TIMEOUT_RATE_CIRCUIT = Math.max(0, Math.min(1, Number(process.env.HERMES_OWNER_MINIMAX_TIMEOUT_RATE_CIRCUIT || 0.4)));
const OWNER_SEARCH_RESULT_TIMEOUT_SECONDS = Math.max(3, Math.min(20, Number(process.env.HERMES_OWNER_SEARCH_RESULT_TIMEOUT_SECONDS || 8)));
const BUSINESS_SOCIAL_FETCH_LIMIT = Math.max(0, Math.min(6, Number(process.env.HERMES_BUSINESS_SOCIAL_FETCH_LIMIT || 3)));
const V4_GENERIC_ASSOCIATED_EMAIL_DOMAINS = new Set([
  "gmail.com",
  "yahoo.com",
  "hotmail.com",
  "outlook.com",
  "icloud.com",
  "aol.com",
  "proton.me"
]);

const LEARNED_OWNER_PAGE_PATHS = [
  "/about",
  "/about-us",
  "/team",
  "/our-team",
  "/staff",
  "/leadership",
  "/our-story",
  "/meet-the-owner",
  "/meet-the-team",
  "/contact",
  "/contact-us"
];

const OWNER_AGENT_ROLES = [
  {
    role: "website-leadership",
    hypothesis: "The business website names the owner on about, team, leadership, staff, story, or contact pages.",
    queries: (lead) => [
      `"${lead.name}" owner "${leadSearchLocation(lead)}"`,
      `"${lead.name}" founder "${leadSearchLocation(lead)}"`,
      `"${lead.name}" "our team"`,
      `"${lead.name}" "founded by"`
    ]
  },
  {
    role: "linkedin-person-profile",
    hypothesis: "The owner has a personal LinkedIn profile tied to the business name or local market.",
    queries: (lead, owner) => [
      owner?.name && `site:linkedin.com/in "${owner.name}" "${lead.name}"`,
      `site:linkedin.com/in "${lead.name}" "${leadSearchLocation(lead)}" owner`,
      `site:linkedin.com/in "${lead.name}" founder`,
      `site:linkedin.com/in "${lead.name}" "${lead.category || ""}"`
    ]
  },
  {
    role: "public-social-profile",
    hypothesis: "The owner is named on public Facebook, Instagram, X, or personal profile pages.",
    queries: (lead, owner) => [
      owner?.name && `"${owner.name}" "${lead.name}" Instagram`,
      `"${lead.name}" owner Facebook "${leadSearchLocation(lead)}"`,
      `"${lead.name}" owner Instagram "${leadSearchLocation(lead)}"`,
      ...socialHandleSearchQueries(lead)
    ]
  },
  {
    role: "articles-interviews",
    hypothesis: "Local press, interviews, podcasts, and community articles identify the owner.",
    queries: (lead, owner) => [
      owner?.name && `"${owner.name}" "${lead.name}" interview`,
      `"${lead.name}" owner interview "${leadSearchLocation(lead)}"`,
      `"${lead.name}" founder story`,
      `"${lead.name}" "${lead.category || ""}" "founded by"`
    ]
  },
  {
    role: "business-directories",
    hypothesis: "Chambers, BBB, local directories, and business listings reveal owner or principal names.",
    queries: (lead) => [
      `"${lead.name}" "principal" "${leadSearchLocation(lead)}"`,
      `"${lead.name}" "owner" "BBB"`,
      `"${lead.name}" "chamber" owner`,
      `"${lead.name}" "business license"`
    ]
  },
  {
    role: "business-registry",
    hypothesis: "LLC, DBA, registered-agent, and public corporate registry records may reveal the principal behind the business.",
    queries: (lead) => [
      `"${lead.name}" LLC "registered agent" "${leadSearchLocation(lead)}"`,
      `"${lead.name}" "secretary of state" "${stateFromLead(lead)}"`,
      `"${lead.name}" "owner" "opencorporates"`,
      `"${lead.name}" "bizapedia" "${stateFromLead(lead)}"`
    ]
  },
  {
    role: "trade-license-owner",
    hypothesis: "Licensed trades often expose a license holder, qualifying party, responsible managing employee, master plumber, or contractor principal in state records.",
    queries: (lead, owner) => [
      `"${lead.name}" "contractor license" "${stateFromLead(lead)}"`,
      `"${lead.name}" "license holder" "${stateFromLead(lead)}"`,
      `"${lead.name}" "qualifying party" "${stateFromLead(lead)}"`,
      `"${lead.name}" "responsible managing" "${stateFromLead(lead)}"`,
      /\bplumb/i.test(`${lead.category || ""} ${lead.name || ""}`) && `"${lead.name}" "master plumber" "${stateFromLead(lead)}"`,
      /\bsolar|pv|photovoltaic/i.test(`${lead.category || ""} ${lead.name || ""}`) && `"${lead.name}" "NABCEP" owner`,
      owner?.name && `"${owner.name}" "${lead.name}" "contractor license"`
    ].filter(Boolean)
  },
  {
    role: "nail-salon-license",
    hypothesis: "Nail salons often appear in cosmetology, salon-license, inspection, and local business-license records that name an owner or registrant.",
    queries: (lead) => [
      `"${lead.name}" "cosmetology" "${leadSearchLocation(lead)}"`,
      `"${lead.name}" "salon license" "${stateFromLead(lead)}"`,
      `"${lead.name}" "business license" "${leadSearchLocation(lead)}"`,
      `"${lead.name}" "registered agent" "${stateFromLead(lead)}"`
    ]
  },
  {
    role: "direct-owner-contact",
    hypothesis: "After owner identity is known, public pages may expose an owner email or phone.",
    queries: (lead, owner) => [
      owner?.name && `"${owner.name}" "${lead.name}" email`,
      owner?.name && `"${owner.name}" "${lead.name}" phone`,
      owner?.name && `"${owner.name}" "${lead.website || lead.name}" contact`,
      `"${lead.name}" founder email "${leadSearchLocation(lead)}"`
    ]
  }
];

export class OwnerRabbitHoleAdapter {
  constructor({ scrapling, minimax, linkedinTools, ownerEvidenceTools }) {
    this.scrapling = scrapling;
    this.minimax = minimax;
    this.linkedinTools = linkedinTools;
    this.ownerEvidenceTools = ownerEvidenceTools;
  }

  async findOwnerContact({ lead, initialEnrichment = {}, run }) {
    lead = {
      ...lead,
      businessEmail: lead.businessEmail || lead.email || initialEnrichment.emails?.[0] || "",
      businessPhone: lead.businessPhone || lead.phone || initialEnrichment.phones?.[0] || "",
      ownerAgentSwarmSize: lead.ownerAgentSwarmSize || run?.input?.ownerAgentSwarmSize,
      run
    };
    lead.ownerSearchLocation = inferOwnerSearchLocation(lead, initialEnrichment);
    const evidence = [];
    const candidates = new Map();
    const socialLinks = new Set();
    const profileAndArticleLinks = new Set();
    const businessEmails = new Set([lead.email, ...(initialEnrichment.emails || [])].filter(Boolean));
    const businessPhones = new Set([lead.phone, ...(initialEnrichment.phones || [])].filter(Boolean));
    const ownerEmails = new Set();
    const ownerPhones = new Set();
    const ownerContactEvidence = [];
    const businessOwnedPages = [];

    const homePage = {
      url: lead.website || "",
      title: initialEnrichment.title || "",
      text: initialEnrichment.text || "",
      emails: initialEnrichment.emails || [],
      phones: initialEnrichment.phones || [],
      links: initialEnrichment.links || [],
      socialLinks: initialEnrichment.socialLinks || [],
      source: "business-home"
    };
    businessOwnedPages.push(homePage);
    this.collectEvidence({
      lead,
      evidence,
      candidates,
      socialLinks,
      profileAndArticleLinks,
      page: homePage
    });
    lead.seedSocialHandles = socialHandlesFromUrls(homePage.socialLinks || []);
    await this.applyWebsiteEvidenceToolkit({
      lead,
      initialEnrichment,
      evidence,
      businessEmails,
      businessPhones,
      socialLinks,
      profileAndArticleLinks,
      run
    });
    await this.fetchBusinessSocialEvidence({
      lead,
      evidence,
      candidates,
      socialLinks,
      profileAndArticleLinks,
      businessEmails,
      businessPhones,
      ownerEmails,
      ownerPhones,
      ownerContactEvidence,
      businessOwnedPages,
      run
    });

    const strategy = await this.planStrategy({ lead, evidence, run });
    recordProviderTelemetry(run, strategy);
    promoteHintContactsFromOwnedPage({
      strategy,
      page: homePage,
      lead,
      candidates,
      ownerEmails,
      ownerPhones,
      ownerContactEvidence
    });
    for (const url of budgetedOwnerDiscoveryUrls(ownerDiscoveryUrls(lead.website, strategy.pagePaths), run)) {
      const page = await this.fetchPublicPage(url, { timeout: ownerPageTimeoutSeconds(run) });
      if (!page.ok) continue;
      const businessPage = { ...page, url, source: "business-rabbit-hole" };
      businessOwnedPages.push(businessPage);
      this.collectEvidence({
        lead,
        evidence,
        candidates,
        socialLinks,
        profileAndArticleLinks,
        page: businessPage
      });
      if (isOwnerContactEvidence({ page: { ...page, url }, owner: bestCandidate(candidates), lead, source: "business-rabbit-hole" })) {
        recordOwnerContacts({
          ownerEmails,
          ownerPhones,
          ownerContactEvidence,
          page: { ...page, url },
          owner: bestCandidate(candidates),
          lead,
          emails: page.emails,
          phones: page.phones,
          source: "business-rabbit-hole",
          reason: "business rabbit-hole page names the owner and business near contact evidence"
        });
      } else {
        promoteHintContactsFromOwnedPage({
          strategy,
          page: { ...page, url },
          lead,
          candidates,
          ownerEmails,
          ownerPhones,
          ownerContactEvidence
        });
        (page.emails || []).forEach((email) => businessEmails.add(email));
        (page.phones || []).forEach((phone) => businessPhones.add(phone));
      }
      if (evidence.length >= 6 && candidates.size) break;
    }

    for (const url of strategy.profileUrlHints || []) {
      if (PUBLIC_EVIDENCE_RE.test(url)) profileAndArticleLinks.add(normalizeEvidenceUrl(url));
    }

    const ownerAgentTrace = [];
    const fetchedSearchUrls = new Set();
    await this.runSeedEmailIdentityProbe({
      lead,
      evidence,
      candidates,
      socialLinks,
      profileAndArticleLinks,
      businessEmails,
      businessPhones,
      ownerEmails,
      ownerPhones,
      ownerContactEvidence,
      businessOwnedPages,
      ownerAgentTrace,
      fetchedSearchUrls,
      run
    });
    await this.runOwnerAgentSwarm({
      lead,
      evidence,
      candidates,
      socialLinks,
      profileAndArticleLinks,
      businessEmails,
      businessPhones,
      ownerEmails,
      ownerPhones,
      ownerContactEvidence,
      strategy,
      ownerAgentTrace,
      fetchedSearchUrls,
      run,
      source: "identity-swarm"
    });
    promoteKnownOwnerContactsFromOwnedPages({
      pages: businessOwnedPages,
      lead,
      owner: bestCandidate(candidates),
      ownerEmails,
      ownerPhones,
      ownerContactEvidence
    });

    if (!candidates.size && this.minimax?.configured?.() && ownerMinimaxPlanningAllowed(run)) {
      const aiCandidates = await this.extractOwnerCandidatesWithAi({ lead, evidence, run });
      for (const candidate of aiCandidates) {
        addCandidate(candidates, {
          name: candidate.name,
          title: candidate.title,
          evidenceUrl: candidate.evidenceUrl,
          score: 70,
          reason: "ai-owner-extraction"
        });
        for (const url of candidate.socialProfiles || []) socialLinks.add(normalizeEvidenceUrl(url));
      }
    }

    const ownerAssociatedStopLoss = v4OwnerAssociatedStopLoss({ run, lead, candidates, businessEmails, businessPhones });
    if (ownerAssociatedStopLoss.ready) {
      run.counters ||= {};
      run.counters.ownerAssociatedStopLosses = (run.counters.ownerAssociatedStopLosses || 0) + 1;
      ownerAgentTrace.push({
        id: "owner-associated-stop-loss",
        phase: "profile-contact-swarm",
        role: "owner-associated-stop-loss",
        hypothesis: "V4 skipped the second direct-contact swarm because a strong owner identity and clean business channel were already proven.",
        queries: [],
        planner: "owner-first-v4",
        searchHits: 0,
        pagesFetched: 0,
        ownerCandidates: [ownerAssociatedStopLoss.ownerName],
        socialLinks: [],
        ownerAssociatedStopLoss: true
      });
    } else if (bestCandidate(candidates)?.name || strategyOwnerNameHints(strategy).length) {
      await this.runOwnerAgentSwarm({
        lead,
        evidence,
        candidates,
        socialLinks,
        profileAndArticleLinks,
        businessEmails,
        businessPhones,
        ownerEmails,
        ownerPhones,
        ownerContactEvidence,
        strategy: {
          ...strategy,
          searchQueries: ownerProfileSearchQueries(lead, bestCandidate(candidates), strategy),
          strategyReason: "Second-pass swarm uses the best owner candidate to search owner profiles and owner-level contact."
        },
        ownerAgentTrace,
        fetchedSearchUrls,
        run,
        source: "profile-contact-swarm"
      });
      promoteKnownOwnerContactsFromOwnedPages({
        pages: businessOwnedPages,
        lead,
        owner: bestCandidate(candidates),
        ownerEmails,
        ownerPhones,
        ownerContactEvidence
      });
    } else {
      run.counters ||= {};
      run.counters.ownerIdentityGateRejections = (run.counters.ownerIdentityGateRejections || 0) + 1;
      ownerAgentTrace.push({
        id: "owner-identity-gate",
        phase: "profile-contact-swarm",
        role: "owner-identity-gate",
        hypothesis: "Skipped direct owner-contact swarm because no owner identity was found in first-pass evidence.",
        queries: [],
        planner: "deterministic-gate",
        searchHits: 0,
        pagesFetched: 0,
        ownerCandidates: [],
        socialLinks: []
      });
    }

    let owner = bestCandidate(candidates);
    await this.applyOwnerProfileToolkit({
      lead,
      owner,
      evidence,
      socialLinks,
      profileAndArticleLinks,
      ownerAgentTrace,
      run
    });
    const ownerProfiles = selectOwnerProfiles(
      [...socialLinks, ...profileAndArticleLinks, ...(strategy.profileUrlHints || [])],
      owner,
      lead
    ).slice(0, 10);
    const socialEvidence = [];

    for (const profileUrl of ownerProfiles) {
      const profile = await this.fetchPublicPage(profileUrl, { timeout: ownerProfileTimeoutSeconds(run), lead, owner });
      socialEvidence.push({
        url: profileUrl,
        ok: Boolean(profile.ok),
        title: profile.title || "",
        emailCount: profile.emails?.length || 0,
        phoneCount: profile.phones?.length || 0,
        toolchain: profile.toolchain || [],
        toolAttempts: profile.toolAttempts || [],
        error: profile.error || ""
      });
      if (!profile.ok) continue;
      const profileContactIsOwnerLevel = isOwnerContactEvidence({
        page: { ...profile, url: profileUrl },
        owner,
        lead,
        source: "owner-profile-fetch"
      });
      this.collectEvidence({
        lead,
        evidence,
        candidates,
        socialLinks,
        profileAndArticleLinks,
        page: { ...profile, url: profileUrl, source: "owner-contact-page" }
      });
      if (profileContactIsOwnerLevel) {
        recordOwnerContacts({
          ownerEmails,
          ownerPhones,
          ownerContactEvidence,
          page: { ...profile, url: profileUrl },
          owner,
          lead,
          emails: profile.emails,
          phones: profile.phones,
          source: "owner-profile-fetch",
          reason: "selected owner profile/article includes owner and business signals"
        });
      } else {
        for (const email of profile.emails || []) businessEmails.add(email);
        for (const phone of profile.phones || []) businessPhones.add(phone);
      }
    }

    owner = bestCandidate(candidates);
    if (owner && personNameLooksLikeBusiness(owner.name, lead)) owner = null;
    const validOwnerContactEvidence = ownerContactEvidence.filter((entry) => ownerEvidenceHasPersonName(entry, lead));
    const cleanOwnerEmails = uniqueBy(
      validOwnerContactEvidence
        .filter((entry) => entry.type === "email")
        .map((entry) => String(entry.value || "").trim().toLowerCase())
        .filter(isUsableEmail),
      (email) => email
    );
    const cleanOwnerPhones = uniqueBy(
      validOwnerContactEvidence
        .filter((entry) => entry.type === "phone")
        .map((entry) => normalizePhone(entry.value))
        .filter(Boolean),
      (phone) => phone
    );
    const cleanOwnerContactEvidence = validOwnerContactEvidence
      .filter((entry) => (entry.type === "email" && cleanOwnerEmails.includes(String(entry.value || "").toLowerCase())) || (entry.type === "phone" && cleanOwnerPhones.includes(normalizePhone(entry.value))))
      .slice(0, 20);
    const businessEmail = [...businessEmails].find((email) => isUsableEmail(email) && !cleanOwnerEmails.includes(String(email).toLowerCase())) || "";
    const businessPhone = [...businessPhones]
      .map(normalizePhone)
      .find((phone) => phone && !cleanOwnerPhones.includes(phone)) || "";
    rememberOwnerRabbitHoleLearning(run, {
      lead,
      status: cleanOwnerEmails.length || cleanOwnerPhones.length ? "owner-contact-found" : "owner-contact-not-found",
      ownerContactEvidence: cleanOwnerContactEvidence,
      ownerAgentTrace
    });

    return {
      businessEmail,
      businessPhone,
      ownerName: owner?.name || "",
      ownerTitle: owner?.title || "",
      ownerEvidenceUrl: owner?.evidenceUrl || "",
      ownerContactEvidence: cleanOwnerContactEvidence,
      ownerSocialProfiles: ownerProfiles,
      ownerLinkedIn: ownerProfiles.find((url) => LINKEDIN_PROFILE_RE.test(url)) || "",
      ownerEmail: cleanOwnerEmails[0] || "",
      ownerPhone: cleanOwnerPhones[0] || "",
      ownerEmails: cleanOwnerEmails,
      ownerPhones: cleanOwnerPhones,
      ownerContactSource: cleanOwnerEmails.length || cleanOwnerPhones.length ? "owner-public-profile-or-article" : "not-found",
      ownerAgentSwarm: {
        requested: ownerAgentConcurrency(lead),
        requestedPerPass: ownerAgentConcurrency(lead),
        maxPerPass: MAX_OWNER_AGENTS,
        deployed: ownerAgentTrace.length,
        deployedTotal: ownerAgentTrace.length,
        traceLimit: OWNER_AGENT_TRACE_LIMIT,
        tracesTruncated: ownerAgentTrace.length > OWNER_AGENT_TRACE_LIMIT,
        summary: summarizeOwnerAgentTrace(ownerAgentTrace),
        traces: ownerAgentTrace.slice(0, OWNER_AGENT_TRACE_LIMIT)
      },
      rabbitHoleStrategy: {
        pagePaths: strategy.pagePaths || [],
        searchQueries: strategy.searchQueries || [],
        profileUrlHints: strategy.profileUrlHints || [],
        ownerNameHints: strategy.ownerNameHints || [],
        strategyReason: strategy.strategyReason || ""
      },
      rabbitHoleStatus: ownerProfiles.length
        ? cleanOwnerEmails.length || cleanOwnerPhones.length
          ? "owner-contact-found"
          : "owner-profile-found-no-contact"
        : owner?.name
          ? "owner-found-no-profile"
          : "owner-not-found",
      ownerEvidence: evidence.slice(0, 14),
      socialEvidence
    };
  }

  collectEvidence({ lead, evidence, candidates, socialLinks, profileAndArticleLinks, page }) {
    const text = normalizeText(page.text || "");
    const relevant = pageRelevantToLead(page, lead);
    const allLinks = [...(page.socialLinks || []), ...(page.links || [])].map(normalizeEvidenceUrl).filter(Boolean);
    const social = uniqueBy(allLinks.filter((url) => SOCIAL_RE.test(url)), (url) => url);
    for (const url of social) socialLinks.add(url);
    for (const url of allLinks.filter((link) => PUBLIC_EVIDENCE_RE.test(link))) profileAndArticleLinks.add(url);

    const found = relevant ? extractOwnerCandidates(text, page.url) : [];
    for (const candidate of found) addCandidate(candidates, candidate);

    if (text || social.length || found.length) {
      evidence.push({
        source: page.source || "page",
        url: page.url || "",
        title: page.title || "",
        ownerCandidates: found.map((candidate) => candidate.name),
        socialLinks: social.slice(0, 12),
        textSample: text.slice(0, 800)
      });
    }
  }

  async fetchPublicPage(url, options = {}) {
    if (!url || !this.scrapling?.available?.()) return { ok: false, error: "scrapling unavailable", emails: [], phones: [], text: "", links: [], socialLinks: [] };
    if (LINKEDIN_PROFILE_RE.test(url) && this.linkedinTools) {
      return this.linkedinTools.enrichProfile({ url, timeout: options.timeout || 28, lead: options.lead || {}, owner: options.owner || {} });
    }
    return this.scrapling.enrich({ url, mode: "simple", timeout: options.timeout || 25 });
  }

  async applyWebsiteEvidenceToolkit({ lead, initialEnrichment, evidence, businessEmails, businessPhones, socialLinks, profileAndArticleLinks, run }) {
    if (!this.ownerEvidenceTools?.available?.().ok || !lead.website) return;
    const result = await this.ownerEvidenceTools.enrichWebsite({
      lead,
      enrichment: initialEnrichment,
      timeout: ownerSearchResultTimeoutSeconds(run),
      run
    });
    run.counters ||= {};
    run.counters.ownerEvidenceToolRuns = (run.counters.ownerEvidenceToolRuns || 0) + 1;
    run.ownerEvidenceToolAttempts ||= [];
    run.ownerEvidenceToolAttempts.push({
      lead: lead.name || "",
      mode: "website",
      ok: Boolean(result.ok),
      toolPolicy: result.toolPolicy || {},
      toolAttempts: result.toolAttempts || []
    });
    run.ownerEvidenceToolAttempts = run.ownerEvidenceToolAttempts.slice(-100);
    for (const email of result.emails || []) businessEmails.add(email);
    for (const phone of result.phones || []) businessPhones.add(phone);
    for (const url of result.profileLinks || []) {
      const clean = normalizeEvidenceUrl(url);
      if (!clean) continue;
      if (SOCIAL_RE.test(clean)) socialLinks.add(clean);
      if (PUBLIC_EVIDENCE_RE.test(clean)) profileAndArticleLinks.add(clean);
    }
    for (const url of result.links || []) {
      const clean = normalizeEvidenceUrl(url);
      if (!clean || !PUBLIC_EVIDENCE_RE.test(clean)) continue;
      profileAndArticleLinks.add(clean);
    }
    for (const item of result.evidence || []) {
      evidence.push({
        source: item.source || "owner-evidence-toolkit",
        url: item.url || lead.website || "",
        title: item.tool || "Owner evidence toolkit",
        ownerCandidates: [],
        socialLinks: (item.profileLinks || item.links || []).filter((url) => SOCIAL_RE.test(url)).slice(0, 12),
        textSample: item.textSample || ""
      });
    }
  }

  async fetchBusinessSocialEvidence({ lead, evidence, candidates, socialLinks, profileAndArticleLinks, businessEmails, businessPhones, ownerEmails, ownerPhones, ownerContactEvidence, businessOwnedPages, run }) {
    if (!BUSINESS_SOCIAL_FETCH_LIMIT || !this.scrapling?.available?.()) return;
    const urls = selectBusinessSocialUrls([...socialLinks, ...profileAndArticleLinks], lead).slice(0, BUSINESS_SOCIAL_FETCH_LIMIT);
    if (!urls.length) return;
    const pages = await mapLimit(urls, Math.min(2, urls.length), async (url) => {
      const page = await this.fetchPublicPage(url, { timeout: ownerSocialPageTimeoutSeconds(run), lead, owner: bestCandidate(candidates) || {} });
      return page?.ok ? { url, page } : null;
    });
    for (const item of pages.filter(Boolean)) {
      const { url, page } = item;
      const socialPage = { ...page, url, source: "business-social-profile" };
      businessOwnedPages.push(socialPage);
      profileAndArticleLinks.add(url);
      if (SOCIAL_RE.test(url)) socialLinks.add(url);
      this.collectEvidence({
        lead,
        evidence,
        candidates,
        socialLinks,
        profileAndArticleLinks,
        page: socialPage
      });
      const owner = bestCandidate(candidates);
      if (isOwnerContactEvidence({ page: socialPage, owner, lead, source: "business-social-profile" })) {
        recordOwnerContacts({
          ownerEmails,
          ownerPhones,
          ownerContactEvidence,
          page: socialPage,
          owner,
          lead,
          emails: page.emails,
          phones: page.phones,
          source: "business-social-profile",
          reason: "official business social profile linked owner identity and contact evidence"
        });
      } else {
        (page.emails || []).forEach((email) => businessEmails.add(email));
        (page.phones || []).forEach((phone) => businessPhones.add(phone));
      }
    }
  }

  async applyOwnerProfileToolkit({ lead, owner, evidence, socialLinks, profileAndArticleLinks, ownerAgentTrace, run }) {
    if (!owner?.name || !this.ownerEvidenceTools?.available?.().ok) return;
    const result = await this.ownerEvidenceTools.findOwnerProfiles({
      lead,
      owner,
      timeout: ownerSearchResultTimeoutSeconds(run) * 3,
      run
    });
    run.counters ||= {};
    run.counters.ownerEvidenceToolRuns = (run.counters.ownerEvidenceToolRuns || 0) + 1;
    run.ownerEvidenceToolAttempts ||= [];
    run.ownerEvidenceToolAttempts.push({
      lead: lead.name || "",
      owner: owner.name || "",
      mode: "profiles",
      ok: Boolean(result.ok),
      usernames: result.usernames || [],
      toolPolicy: result.toolPolicy || {},
      toolAttempts: result.toolAttempts || []
    });
    run.ownerEvidenceToolAttempts = run.ownerEvidenceToolAttempts.slice(-100);
    const links = uniqueBy([...(result.profileLinks || []), ...(result.links || [])], (url) => url);
    for (const url of links) {
      const clean = normalizeEvidenceUrl(url);
      if (!clean) continue;
      if (SOCIAL_RE.test(clean)) socialLinks.add(clean);
      if (PUBLIC_EVIDENCE_RE.test(clean)) profileAndArticleLinks.add(clean);
    }
    for (const item of result.evidence || []) {
      evidence.push({
        source: item.source || "owner-profile-toolkit",
        url: "",
        title: `${item.tool || "Owner profile toolkit"} ${item.username || ""}`.trim(),
        ownerCandidates: [owner.name],
        socialLinks: (item.profileLinks || []).slice(0, 12),
        textSample: item.textSample || ""
      });
    }
    ownerAgentTrace.push({
      id: "owner-profile-toolkit",
      phase: "owner-profile-toolkit",
      role: "maigret-sherlock-social-analyzer",
      hypothesis: "Username-based OSINT tools may find public owner social profiles tied to the owner or business handle.",
      queries: result.usernames || [],
      planner: "toolkit",
      searchHits: links.length,
      pagesFetched: 0,
      ownerCandidates: owner?.name ? [owner.name] : [],
      socialLinks: links.slice(0, 12)
    });
  }

  async planStrategy({ lead, evidence, run }) {
    const fallback = fallbackOwnerStrategy(lead, run);
    if (!ownerMinimaxPlanningAllowed(run) || !this.minimax?.configured?.() || typeof this.minimax.proposeOwnerRabbitHoleStrategy !== "function") return fallback;
    const strategy = await this.minimax.proposeOwnerRabbitHoleStrategy({
      lead,
      evidence,
      priorAttempts: run?.ownerRabbitHoleAttempts?.[leadKeyForStrategy(lead)] || [],
      timeoutMs: ownerMinimaxPlanningTimeoutMs(run)
    });
    return normalizeStrategy(strategy, fallback);
  }

  async runSeedEmailIdentityProbe({ lead, evidence, candidates, socialLinks, profileAndArticleLinks, businessEmails, businessPhones, ownerEmails, ownerPhones, ownerContactEvidence, businessOwnedPages, ownerAgentTrace, fetchedSearchUrls, run }) {
    if (candidates.size || ownerEmails.size || ownerPhones.size) return;
    const queries = seedEmailSearchQueries(lead).slice(0, SEED_EMAIL_PROBE_QUERY_LIMIT);
    if (!queries.length) return;

    const trace = {
      id: "seed-email-identity-probe-1",
      phase: "seed-email-identity-probe",
      role: "seed-email-identity-probe",
      hypothesis: "A personal-looking seed email from the business site may identify the owner in public search before the full swarm runs.",
      queries,
      planner: "deterministic",
      searchHits: 0,
      pagesFetched: 0,
      ownerCandidates: [],
      socialLinks: []
    };
    await this.runPublicSearchPlan({
      lead,
      evidence,
      candidates,
      socialLinks,
      profileAndArticleLinks,
      businessEmails,
      businessPhones,
      ownerEmails,
      ownerPhones,
      ownerContactEvidence,
      fetchedSearchUrls,
      queries,
      source: "seed-email-identity-probe:owner-identity",
      trace,
      run
    });
    promoteKnownOwnerContactsFromOwnedPages({
      pages: businessOwnedPages,
      lead,
      owner: bestCandidate(candidates),
      ownerEmails,
      ownerPhones,
      ownerContactEvidence
    });
    ownerAgentTrace.push(trace);
  }

  async runOwnerAgentSwarm({ lead, evidence, candidates, socialLinks, profileAndArticleLinks, businessEmails, businessPhones, ownerEmails, ownerPhones, ownerContactEvidence, strategy, ownerAgentTrace, fetchedSearchUrls, run, source }) {
    const owner = bestCandidate(candidates);
    const concurrency = ownerAgentConcurrency({ ...lead, ownerIdentityKnown: Boolean(owner?.name) });
    const agents = await this.planOwnerAgents({ lead, owner, strategy, evidence, source, requestedAgents: concurrency, run });
    if (run && agents.length) {
      run.counters ||= {};
      run.counters.ownerAgentsStarted = (run.counters.ownerAgentsStarted || 0) + agents.length;
    }
    const results = await mapLimit(agents, Math.min(OWNER_AGENT_EXECUTION_CONCURRENCY, agents.length || 1), async (agent) => {
      const trace = {
        id: agent.id,
        phase: source,
        role: agent.role,
        hypothesis: agent.hypothesis,
        queries: agent.queries,
        planner: agent.planner || "template",
        searchHits: 0,
        pagesFetched: 0,
        ownerCandidates: [],
        socialLinks: []
      };
      await this.runPublicSearchPlan({
        lead,
        evidence,
        candidates,
        socialLinks,
        profileAndArticleLinks,
        businessEmails,
        businessPhones,
        ownerEmails,
        ownerPhones,
        ownerContactEvidence,
        fetchedSearchUrls,
        queries: agent.queries,
        source: `${source}:${agent.role}`,
        trace,
        run
      });
      return trace;
    });
    ownerAgentTrace.push(...results.filter(Boolean));
  }

  async planOwnerAgents({ lead, owner, strategy, evidence, source, requestedAgents, run }) {
    const fallbackAgents = buildOwnerAgents({ lead, owner, strategy, source, run });
    if (!ownerMinimaxPlanningAllowed(run) || !this.minimax?.configured?.() || typeof this.minimax.proposeOwnerAgentSwarm !== "function") {
      return fallbackAgents.slice(0, requestedAgents);
    }

    const planned = await this.minimax.proposeOwnerAgentSwarm({
      lead,
      owner,
      strategy,
      evidence,
      phase: source,
      requestedAgents,
      timeoutMs: ownerMinimaxPlanningTimeoutMs(run)
    });
    recordProviderTelemetry(run, planned);
    const aiAgents = normalizePlannedAgents(planned?.agents, source);
    return rankOwnerAgents(
      uniqueBy([...aiAgents, ...fallbackAgents], (agent) => `${agent.role}:${agent.queries.join("|").toLowerCase()}`),
      { lead, owner, source, run }
    ).slice(0, requestedAgents);
  }

  async runPublicSearchPlan({ lead, evidence, candidates, socialLinks, profileAndArticleLinks, businessEmails, businessPhones, ownerEmails, ownerPhones, ownerContactEvidence, fetchedSearchUrls, queries, source, trace, run }) {
    const queryList = uniqueBy((queries || []).filter(Boolean), (value) => normalizeText(value).toLowerCase()).slice(0, ownerSearchQueryLimit(run));
    const queryResults = await mapLimit(queryList, Math.min(OWNER_SEARCH_QUERY_CONCURRENCY, queryList.length || 1), async (query) => ({
      query,
      results: await searchPublicWeb(query)
    }));

    for (const { query, results } of queryResults) {
      if (trace) trace.searchHits += results.length;
      evidence.push({
        source,
        url: "",
        title: `Search: ${query}`,
        ownerCandidates: [],
        socialLinks: results.map((result) => result.url).filter((url) => PUBLIC_EVIDENCE_RE.test(url)).slice(0, 8),
        textSample: results.map((result) => `${result.title} ${result.snippet}`.trim()).join(" | ").slice(0, 800)
      });

      const resultLimit = ownerSearchResultsPerQuery(run);
      const pageResults = await mapLimit(results.slice(0, resultLimit), Math.max(1, Math.min(OWNER_SEARCH_QUERY_CONCURRENCY, resultLimit)), async (result) => {
        const url = normalizeEvidenceUrl(result.url);
        if (!url || (!PUBLIC_EVIDENCE_RE.test(url) && !shouldFetchOwnerSearchResult({ source, trace, result }))) return null;
        addSearchResultOwnerCandidates({
          lead,
          candidates,
          evidence,
          trace,
          result: { ...result, url },
          source
        });
        profileAndArticleLinks.add(url);
        if (SOCIAL_RE.test(url)) socialLinks.add(url);
        if (fetchedSearchUrls?.has(url)) return null;
        fetchedSearchUrls?.add(url);
        const page = await this.fetchPublicPage(url, { timeout: ownerSearchResultTimeoutSeconds(run), lead, owner: bestCandidate(candidates) || {} });
        if (!page.ok) return null;
        return { url, page };
      });

      for (const item of pageResults.filter(Boolean)) {
        const { url, page } = item;
        if (trace) {
          trace.pagesFetched += 1;
          if (SOCIAL_RE.test(url)) trace.socialLinks.push(url);
        }
        const beforeCandidates = new Set([...candidates.keys()]);
        this.collectEvidence({
          lead,
          evidence,
          candidates,
          socialLinks,
          profileAndArticleLinks,
          page: { ...page, url, source: "public-search-result" }
        });
        if (trace) {
          for (const [key, candidate] of candidates) {
            if (!beforeCandidates.has(key)) trace.ownerCandidates.push(candidate.name);
          }
          trace.ownerCandidates = uniqueBy(trace.ownerCandidates, (name) => name.toLowerCase()).slice(0, 12);
          trace.socialLinks = uniqueBy(trace.socialLinks, (url) => url).slice(0, 12);
        }
        const owner = bestCandidate(candidates);
        if (isOwnerContactEvidence({ page: { ...page, url }, owner, lead, source, trace })) {
          recordOwnerContacts({
            ownerEmails,
            ownerPhones,
            ownerContactEvidence,
            page: { ...page, url },
            owner,
            lead,
            emails: page.emails,
            phones: page.phones,
            source,
            reason: "public search result matched owner, business, and owner-contact search intent"
          });
          if (trace) trace.ownerContactPromoted = true;
        } else {
          (page.emails || []).forEach((email) => businessEmails.add(email));
          (page.phones || []).forEach((phone) => businessPhones.add(phone));
        }
      }
    }
  }

  async extractOwnerCandidatesWithAi({ lead, evidence, run }) {
    const fallback = { owners: [] };
    try {
      const result = await this.minimax.chatJson({
        fallback,
        system:
          "Extract business owner identity only from provided public evidence. Do not guess. " +
          "Owner means owner, founder, co-founder, CEO, president, principal, operator, or managing partner. " +
          "Return JSON only.",
        user: JSON.stringify({
          task: "Find likely business owner names and their public social profile URLs.",
          business: {
            name: lead.name,
            website: lead.website,
            city: lead.city,
            category: lead.category
          },
          criteria: run?.input?.criteria || "",
          evidence,
          outputSchema: {
            owners: [{
              name: "owner full name from evidence",
              title: "owner title from evidence",
              evidenceUrl: "source URL",
              socialProfiles: ["public social profile URLs"]
            }]
          }
        })
      });
      return Array.isArray(result.owners) ? result.owners.filter((owner) => owner?.name) : [];
    } catch {
      return [];
    }
  }
}

function summarizeOwnerAgentTrace(traces = []) {
  const summary = {
    searchHits: 0,
    pagesFetched: 0,
    ownerContactPromotions: 0,
    ownerCandidatesFound: 0,
    socialLinksFound: 0,
    byPhase: {}
  };
  const ownerCandidates = new Set();
  const socialLinks = new Set();
  for (const trace of traces || []) {
    summary.searchHits += Number(trace?.searchHits || 0);
    summary.pagesFetched += Number(trace?.pagesFetched || 0);
    if (trace?.ownerContactPromoted) summary.ownerContactPromotions += 1;
    for (const name of trace?.ownerCandidates || []) ownerCandidates.add(String(name).toLowerCase());
    for (const url of trace?.socialLinks || []) socialLinks.add(url);
    const phase = trace?.phase || "owner-swarm";
    summary.byPhase[phase] ||= { agents: 0, searchHits: 0, pagesFetched: 0, ownerContactPromotions: 0 };
    summary.byPhase[phase].agents += 1;
    summary.byPhase[phase].searchHits += Number(trace?.searchHits || 0);
    summary.byPhase[phase].pagesFetched += Number(trace?.pagesFetched || 0);
    if (trace?.ownerContactPromoted) summary.byPhase[phase].ownerContactPromotions += 1;
  }
  summary.ownerCandidatesFound = ownerCandidates.size;
  summary.socialLinksFound = socialLinks.size;
  return summary;
}

function rememberOwnerRabbitHoleLearning(run, { lead = {}, status = "", ownerContactEvidence = [], ownerAgentTrace = [] } = {}) {
  if (!run) return;
  run.ownerContactStrategyLearning ||= {
    version: 1,
    leads: 0,
    contactsFound: 0,
    evidenceSources: {},
    roles: {}
  };
  const learning = run.ownerContactStrategyLearning;
  learning.version = 1;
  learning.leads = Number(learning.leads || 0) + 1;
  if (/found/i.test(status) && ownerContactEvidence.length) learning.contactsFound = Number(learning.contactsFound || 0) + 1;
  for (const entry of ownerContactEvidence || []) {
    const source = normalizeText(entry.source || "unknown") || "unknown";
    learning.evidenceSources[source] ||= { contacts: 0, examples: [] };
    learning.evidenceSources[source].contacts += 1;
    if (learning.evidenceSources[source].examples.length < 5) {
      learning.evidenceSources[source].examples.push({
        name: lead.name || "",
        url: entry.url || "",
        type: entry.type || ""
      });
    }
  }
  for (const trace of ownerAgentTrace || []) {
    const role = normalizeText(trace.role || "unknown") || "unknown";
    learning.roles[role] ||= { traces: 0, promotions: 0, candidates: 0, pages: 0, hits: 0 };
    learning.roles[role].traces += 1;
    learning.roles[role].promotions += trace.ownerContactPromoted ? 1 : 0;
    learning.roles[role].candidates += Array.isArray(trace.ownerCandidates) && trace.ownerCandidates.length ? 1 : 0;
    learning.roles[role].pages += Number(trace.pagesFetched || 0);
    learning.roles[role].hits += Number(trace.searchHits || 0);
  }
  const roleEntries = Object.entries(learning.roles)
    .sort((a, b) => Number(b[1].promotions || 0) - Number(a[1].promotions || 0) || Number(b[1].candidates || 0) - Number(a[1].candidates || 0))
    .slice(0, 40);
  learning.roles = Object.fromEntries(roleEntries);
  learning.lastUpdatedAt = new Date().toISOString();
}

function ownerStrategyLearning(run = {}) {
  const roles = new Map();
  const evidenceSources = new Map();
  const state = {
    roles,
    evidenceSources,
    hasSignals: false,
    businessOwnedContactWins: 0,
    ownerProfileContactWins: 0,
    broadSearchWaste: false
  };

  const stored = run?.ownerContactStrategyLearning || {};
  for (const [role, stats] of Object.entries(stored.roles || {})) {
    mergeRoleStats(roles, role, stats);
  }
  for (const [source, stats] of Object.entries(stored.evidenceSources || {})) {
    mergeEvidenceSource(evidenceSources, source, stats);
  }

  const leads = [...(run?.qualifiedLeads || []), ...(run?.rejectedLeads || [])];
  for (const lead of leads.slice(-250)) {
    for (const entry of lead.ownerContactEvidence || []) {
      mergeEvidenceSource(evidenceSources, entry.source || "unknown", { contacts: 1 });
    }
    for (const trace of lead.ownerAgentSwarm?.traces || []) {
      mergeRoleStats(roles, trace.role || "unknown", {
        traces: 1,
        promotions: trace.ownerContactPromoted ? 1 : 0,
        candidates: Array.isArray(trace.ownerCandidates) && trace.ownerCandidates.length ? 1 : 0,
        pages: Number(trace.pagesFetched || 0),
        hits: Number(trace.searchHits || 0)
      });
    }
  }

  for (const [source, stats] of evidenceSources) {
    const contacts = Number(stats.contacts || 0);
    if (!contacts) continue;
    state.hasSignals = true;
    if (/business-rabbit-hole|owner-name-hint-business-page|known-owner-business-page/i.test(source)) {
      state.businessOwnedContactWins += contacts;
    }
    if (/owner-profile-fetch|owner-public-profile|profile-contact/i.test(source)) {
      state.ownerProfileContactWins += contacts;
    }
  }

  const broadRoles = ["business-directories", "business-registry", "nail-salon-license", "articles-interviews"];
  const broad = broadRoles.map((role) => roles.get(role)).filter(Boolean);
  const broadTraces = broad.reduce((sum, stats) => sum + Number(stats.traces || 0), 0);
  const broadWins = broad.reduce((sum, stats) => sum + Number(stats.promotions || 0) + Number(stats.contacts || 0) + Number(stats.candidates || 0), 0);
  state.broadSearchWaste = broadTraces >= 25 && broadWins === 0;
  if (roles.size || evidenceSources.size) state.hasSignals = true;
  return state;
}

function mergeRoleStats(roles, role, stats = {}) {
  const key = normalizeText(role || "unknown") || "unknown";
  const current = roles.get(key) || { traces: 0, promotions: 0, candidates: 0, pages: 0, hits: 0, contacts: 0 };
  current.traces += Number(stats.traces || 0);
  current.promotions += Number(stats.promotions || 0);
  current.candidates += Number(stats.candidates || 0);
  current.pages += Number(stats.pages || 0);
  current.hits += Number(stats.hits || 0);
  current.contacts += Number(stats.contacts || 0);
  roles.set(key, current);
}

function mergeEvidenceSource(evidenceSources, source, stats = {}) {
  const key = normalizeText(source || "unknown") || "unknown";
  const current = evidenceSources.get(key) || { contacts: 0 };
  current.contacts += Number(stats.contacts || 0);
  evidenceSources.set(key, current);
}

function recordProviderTelemetry(run, result = {}) {
  const meta = result?.providerMeta;
  if (!run || !meta) return;
  run.counters ||= {};
  run.counters.llmCalls = (run.counters.llmCalls || 0) + 1;
  run.counters.llmDurationMs = (run.counters.llmDurationMs || 0) + Number(meta.durationMs || 0);
  const timeoutCount = Number(meta.timeoutCount || (meta.timedOut ? 1 : 0));
  run.counters.llmTimeouts = (run.counters.llmTimeouts || 0) + timeoutCount;
  run.counters.llmConsecutiveTimeoutFallbacks = result.providerFallback && timeoutCount
    ? (run.counters.llmConsecutiveTimeoutFallbacks || 0) + 1
    : 0;
  if (result.providerFallback) {
    run.counters.llmFallbacks = (run.counters.llmFallbacks || 0) + 1;
    run.counters.providerFallbacks = (run.counters.providerFallbacks || 0) + 1;
  }
}

function minimaxCircuitOpen(run = {}) {
  const threshold = Math.max(1, Number(process.env.HERMES_MINIMAX_TIMEOUT_CIRCUIT || 5));
  return Number(run?.counters?.llmConsecutiveTimeoutFallbacks || 0) >= threshold;
}

function ownerMinimaxPlanningAllowed(run = {}) {
  if (process.env.HERMES_OWNER_MINIMAX_PLANNING === "0") return false;
  if (minimaxCircuitOpen(run)) return false;
  if (ownerMinimaxTimeoutRateCircuitOpen(run)) return false;
  const timeoutMs = Number(run?.input?.ownerHuntTimeoutMs || process.env.HERMES_OWNER_HUNT_TIMEOUT_MS || 90000);
  return !Number.isFinite(timeoutMs) || timeoutMs >= OWNER_MINIMAX_PLANNING_MIN_TIMEOUT_MS;
}

function ownerMinimaxTimeoutRateCircuitOpen(run = {}) {
  const calls = Number(run?.counters?.llmCalls || 0);
  const timeouts = Number(run?.counters?.llmTimeouts || 0);
  if (calls <= 0 || timeouts < OWNER_MINIMAX_TIMEOUT_COUNT_CIRCUIT) return false;
  return timeouts / calls >= OWNER_MINIMAX_TIMEOUT_RATE_CIRCUIT;
}

function ownerBudgetMs(run = {}) {
  const timeoutMs = Number(run?.input?.ownerHuntTimeoutMs || process.env.HERMES_OWNER_HUNT_TIMEOUT_MS || 90000);
  return Number.isFinite(timeoutMs) ? timeoutMs : 90000;
}

function ownerMinimaxPlanningTimeoutMs(run = {}) {
  const budget = ownerBudgetMs(run);
  const budgetSlice = Math.max(3000, Math.floor(budget * 0.18));
  return Math.min(OWNER_MINIMAX_PLANNING_TIMEOUT_MS, budgetSlice);
}

function shortOwnerBudget(run = {}) {
  return ownerBudgetMs(run) < OWNER_MINIMAX_PLANNING_MIN_TIMEOUT_MS;
}

function budgetedOwnerDiscoveryUrls(urls = [], run = {}) {
  const unique = uniqueBy(urls, (url) => normalizeText(url).toLowerCase());
  const budget = ownerBudgetMs(run);
  if (budget <= 30000) return unique.slice(0, 2);
  if (budget < OWNER_MINIMAX_PLANNING_MIN_TIMEOUT_MS) return unique.slice(0, 3);
  if (budget <= 90000) return unique.slice(0, 4);
  if (budget <= 120000) return unique.slice(0, 6);
  return unique;
}

function ownerPageTimeoutSeconds(run = {}) {
  return shortOwnerBudget(run) ? 4 : 22;
}

function ownerProfileTimeoutSeconds(run = {}) {
  return shortOwnerBudget(run) ? 6 : 28;
}

function ownerSearchResultTimeoutSeconds(run = {}) {
  return shortOwnerBudget(run) ? Math.min(5, OWNER_SEARCH_RESULT_TIMEOUT_SECONDS) : OWNER_SEARCH_RESULT_TIMEOUT_SECONDS;
}

function ownerSocialPageTimeoutSeconds(run = {}) {
  return shortOwnerBudget(run) ? 4 : Math.min(7, OWNER_SEARCH_RESULT_TIMEOUT_SECONDS);
}

function ownerDiscoveryUrls(website, pagePaths = []) {
  const root = normalizeUrl(website);
  if (!root) return [];
  const base = new URL(root);
  base.pathname = "/";
  base.search = "";
  base.hash = "";
  const origin = base.toString().replace(/\/$/, "");
  const paths = uniqueBy([
    "",
    ...pagePaths,
    "/about",
    "/about-us",
    "/team",
    "/our-team",
    "/staff",
    "/leadership",
    "/our-story",
    "/contact",
    "/contact-us"
  ], (path) => String(path).toLowerCase());
  return paths.map((path) => {
    const text = String(path || "");
    if (/^https?:\/\//i.test(text)) return normalizeUrl(text);
    return `${origin}${text.startsWith("/") ? text : `/${text}`}`;
  }).filter((url) => url && !isStaticAssetUrl(url));
}

function fallbackOwnerStrategy(lead = {}, run = {}) {
  const name = primaryBusinessName(lead.name || "business");
  const names = businessNameVariants(lead.name || name);
  const city = leadSearchLocation(lead);
  const state = stateFromLead(lead);
  const category = normalizeText(lead.category || "");
  const emailQueries = seedEmailSearchQueries(lead, { names, city });
  const learning = ownerStrategyLearning(run);
  const learnedPagePaths = learning.businessOwnedContactWins ? LEARNED_OWNER_PAGE_PATHS : LEARNED_OWNER_PAGE_PATHS.slice(0, 9);
  const directContactQueries = names.flatMap((candidateName) => [
    `"${candidateName}" "meet the owner" ${city}`.trim(),
    `"${candidateName}" "contact the owner"`.trim(),
    `"${candidateName}" founder email ${city}`.trim(),
    `"${candidateName}" owner email`.trim(),
    `"${candidateName}" owner phone`.trim(),
    `"${candidateName}" "founded by"` .trim()
  ]);
  const profileQueries = names.flatMap((candidateName) => [
    `site:linkedin.com/in "${candidateName}" ${city}`.trim(),
    `site:facebook.com "${candidateName}" ${city}`.trim(),
    `site:instagram.com "${candidateName}" ${city}`.trim(),
    `"${candidateName}" "${category}" owner interview ${city}`.trim()
  ]);
  const nameQueries = names.flatMap((candidateName) => [
    `"${candidateName}" owner ${city}`.trim(),
    `"${candidateName}" founder ${city}`.trim(),
    `"${candidateName}" "registered agent" ${state}`.trim(),
    `"${candidateName}" "business license" ${city}`.trim(),
    `"${candidateName}" "contractor license" ${state}`.trim(),
    `"${candidateName}" "license holder" ${state}`.trim(),
    `"${candidateName}" "qualifying party" ${state}`.trim(),
    `"${candidateName}" "master plumber" ${state}`.trim(),
    `"${candidateName}" "secretary of state" ${state}`.trim(),
    `"${candidateName}" "salon license" ${state}`.trim(),
    `"${candidateName}" "NABCEP" owner`.trim(),
    `"${candidateName}" "opencorporates"` .trim(),
    `"${candidateName}" "bizapedia" ${state}`.trim()
  ]);
  return {
    pagePaths: learnedPagePaths,
    searchQueries: [
      ...directContactQueries,
      ...profileQueries,
      ...socialHandleSearchQueries(lead),
      ...emailQueries,
      ...nameQueries
    ].filter(Boolean),
    profileUrlHints: [],
    ownerNameHints: [],
    strategyReason: learning.hasSignals
      ? "Adaptive fallback strategy prioritizes business-owned owner pages, exact owner-contact queries, and owner-profile fetches because prior run evidence converted there; broad directory/license searches are delayed unless niche-specific."
      : "Fallback owner strategy prioritizes business-owned owner pages, exact owner-contact queries, owner profile search, and article/interview queries before broad directory/license searches."
  };
}

function buildOwnerAgents({ lead, owner, strategy = {}, source = "owner-swarm", run = {} }) {
  const baseQueries = arrayOfStrings(strategy.searchQueries);
  const queryLead = { ...lead, name: primaryBusinessName(lead.name || "") };
  const agents = [];
  let index = 1;

  for (const role of OWNER_AGENT_ROLES) {
    const queries = uniqueBy(role.queries(queryLead, owner).filter(Boolean), (query) => normalizeText(query).toLowerCase()).slice(0, 4);
    if (!queries.length) continue;
    agents.push({
      id: `${source}-${index++}`,
      role: role.role,
      hypothesis: role.hypothesis,
      queries
    });
  }

  for (const queries of chunk(baseQueries, 3)) {
    agents.push({
      id: `${source}-${index++}`,
      role: "minimax-strategy",
      hypothesis: strategy.strategyReason || "MiniMax/fallback strategy supplied targeted owner-contact search queries.",
      queries
    });
  }

  const expandedQueries = expandOwnerAgentQueries({ lead, owner, baseQueries });
  for (const queries of chunk(expandedQueries, 3)) {
    if (agents.length >= MAX_OWNER_AGENTS) break;
    agents.push({
      id: `${source}-${index++}`,
      role: "adaptive-rabbit-hole",
      hypothesis: "Probe a distinct public-web angle for owner identity, owner profile, or owner contact evidence.",
      queries
    });
  }

  return rankOwnerAgents(
    uniqueBy(agents, (agent) => `${agent.role}:${agent.queries.join("|").toLowerCase()}`),
    { lead, owner, source, run }
  ).slice(0, MAX_OWNER_AGENTS);
}

function rankOwnerAgents(agents = [], { lead = {}, owner = {}, source = "", run = {} } = {}) {
  const learning = ownerStrategyLearning(run);
  const categoryText = normalizeText(`${lead.category || ""} ${lead.name || ""}`);
  const ownerKnown = Boolean(owner?.name);
  const isProfilePass = /profile-contact-swarm/i.test(source);
  const scored = agents.map((agent, index) => {
    let score = 100 - index;
    if (agent.planner === "minimax") score += Number(agent.priority || 50);
    if (agent.role === "website-leadership") score += learning.businessOwnedContactWins ? 90 : 55;
    if (agent.role === "direct-owner-contact") score += ownerKnown || isProfilePass ? 120 : 45;
    if (agent.role === "linkedin-person-profile") score += ownerKnown || learning.ownerProfileContactWins ? 85 : 35;
    if (agent.role === "public-social-profile") score += ownerKnown || learning.ownerProfileContactWins ? 50 : 15;
    if (agent.role === "articles-interviews") score += 12;
    if (agent.role === "business-directories") score -= learning.broadSearchWaste ? 35 : 12;
    if (agent.role === "business-registry") score -= learning.broadSearchWaste ? 35 : 8;
    if (agent.role === "trade-license-owner") score += /\b(plumb|contract|roof|hvac|electric|solar|construction|repair)\b/i.test(categoryText) ? 35 : -25;
    if (agent.role === "nail-salon-license") score += /\b(nail|salon|cosmetology|spa)\b/i.test(categoryText) ? 35 : -25;
    const roleStats = learning.roles.get(agent.role);
    if (roleStats?.contacts || roleStats?.promotions) score += 80 + (roleStats.contacts || 0) * 10 + (roleStats.promotions || 0) * 10;
    if (roleStats?.candidates && !roleStats?.contacts) score += Math.min(35, roleStats.candidates * 4);
    if (roleStats?.traces >= 20 && !roleStats?.contacts && !roleStats?.promotions && !roleStats?.candidates) score -= 45;
    return { agent, index, score };
  });
  return scored
    .sort((a, b) => b.score - a.score || a.index - b.index)
    .map((entry, index) => ({
      ...entry.agent,
      adaptivePriority: Math.round(entry.score),
      adaptiveRank: index + 1
    }));
}

function v4OwnerAssociatedStopLoss({ run = {}, lead = {}, candidates, businessEmails = new Set(), businessPhones = new Set() } = {}) {
  if (process.env.HERMES_V4_ASSOCIATED_STOP_LOSS === "0") return { ready: false };
  if (run?.input?.strategyVersion !== "owner-first-v4") return { ready: false };
  const owner = bestCandidate(candidates);
  if (!owner?.name || !isLikelyPersonName(owner.name) || personNameLooksLikeBusiness(owner.name, lead)) return { ready: false };
  const email = [...businessEmails].find((value) => v4OfficialAssociatedEmail(value, lead));
  const phone = [...businessPhones].map(v4UsBusinessPhone).find(Boolean);
  if (!email && !phone) return { ready: false };
  return { ready: true, ownerName: owner.name, email: email || "", phone: phone || "" };
}

function v4OfficialAssociatedEmail(value = "", lead = {}) {
  const email = String(value || "").trim().toLowerCase();
  if (!isUsableEmail(email)) return "";
  const domain = email.split("@")[1] || "";
  if (V4_GENERIC_ASSOCIATED_EMAIL_DOMAINS.has(domain)) return "";
  if (/\b(xnxx|casino|bonus|payout|loan|crypto|pharma|adult)\b/i.test(domain)) return "";
  const host = hostFromUrl(lead.website || "");
  return host && domainMatchesHost(domain, host) ? email : "";
}

function v4UsBusinessPhone(value = "") {
  const normalized = normalizePhone(value);
  const digits = normalized.replace(/\D/g, "");
  const local = digits.length === 11 && digits.startsWith("1") ? digits.slice(1) : digits;
  if (local.length !== 10) return "";
  if (!/^[2-9]\d{2}[2-9]\d{6}$/.test(local)) return "";
  if (/^(\d)\1{9}$/.test(local)) return "";
  return normalized;
}

function normalizePlannedAgents(agents, source) {
  if (!Array.isArray(agents)) return [];
  return agents.map((agent, index) => {
    const role = slugAgentRole(agent?.role || `planned-agent-${index + 1}`);
    const queries = arrayOfStrings(agent?.queries).slice(0, 5);
    if (!queries.length) return null;
    return {
      id: `${source}-ai-${index + 1}`,
      role,
      hypothesis: normalizeText(agent?.hypothesis || "AI-planned public owner-contact search agent."),
      queries,
      priority: Number(agent?.priority || 50),
      planner: "minimax"
    };
  }).filter(Boolean)
    .sort((a, b) => b.priority - a.priority)
    .slice(0, MAX_OWNER_AGENTS);
}

function slugAgentRole(value) {
  const slug = normalizeText(value)
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/^-|-$/g, "");
  return slug || "planned-agent";
}

function expandOwnerAgentQueries({ lead, owner, baseQueries }) {
  const name = primaryBusinessName(lead.name || "");
  const names = businessNameVariants(lead.name || name);
  const city = leadSearchLocation(lead);
  const state = stateFromLead(lead);
  const category = normalizeText(lead.category || "");
  const websiteHost = hostFromUrl(lead.website);
  const ownerName = normalizeText(owner?.name || "");
  const emailQueries = seedEmailSearchQueries(lead, { names, city });
  const seeds = [
    ...baseQueries,
    ...emailQueries,
    ...names.flatMap((candidateName) => [
      `"${candidateName}" owner email`,
      `"${candidateName}" owner phone`,
      `"${candidateName}" founder contact`,
      `"${candidateName}" CEO contact`,
      `"${candidateName}" principal contact`,
      `"${candidateName}" owner LinkedIn "${city}"`,
      `"${candidateName}" founder LinkedIn "${city}"`,
      `"${candidateName}" owner Facebook "${city}"`,
      `"${candidateName}" owner Instagram "${city}"`,
      `"${candidateName}" "${category}" owner`,
      `"${candidateName}" "meet the owner"`,
      `"${candidateName}" "founded by"`,
      `"${candidateName}" "small business owner"`,
      `"${candidateName}" "press release" owner`,
      `"${candidateName}" "voyage" owner`,
      `"${candidateName}" "canvasrebel" owner`,
      `"${candidateName}" "substack" owner`,
      `"${candidateName}" "medium.com" owner`,
      `"${candidateName}" "youtube.com" owner`,
      `"${candidateName}" "about.me" owner`,
      `"${candidateName}" "registered agent"`,
      `"${candidateName}" "secretary of state" "${state}"`,
      `"${candidateName}" "business license" "${city}"`,
      `"${candidateName}" "contractor license" "${state}"`,
      `"${candidateName}" "license holder" "${state}"`,
      `"${candidateName}" "qualifying party" "${state}"`,
      `"${candidateName}" "responsible managing" "${state}"`,
      `"${candidateName}" "master plumber" "${state}"`,
      `"${candidateName}" "cosmetology" "${state}"`,
      `"${candidateName}" "salon license" "${state}"`,
      `"${candidateName}" "NABCEP" owner`,
      `"${candidateName}" "LLC" owner`,
      `"${candidateName}" "DBA" owner`,
      `"${candidateName}" "opencorporates"`,
      `"${candidateName}" "bizapedia" Texas`,
      `"${candidateName}" "corporationwiki"`
    ]),
    websiteHost && `"${websiteHost}" owner`,
    websiteHost && `"${websiteHost}" email`,
    websiteHost && `"${websiteHost}" "registered agent"`,
    ...socialHandleSearchQueries(lead),
    ownerName && `"${ownerName}" email`,
    ownerName && `"${ownerName}" phone`,
    ownerName && `"${ownerName}" LinkedIn`,
    ownerName && `"${ownerName}" "${city}" "${category}"`
  ].filter(Boolean);
  const profileSites = [
    "linkedin.com/in",
    "facebook.com",
    "instagram.com",
    "x.com",
    "twitter.com",
    "tiktok.com",
    "crunchbase.com",
    "alignable.com",
    "bbb.org",
    "chamberofcommerce.com",
    "opencorporates.com",
    "bizapedia.com",
    "corporationwiki.com",
    "buzzfile.com",
    "manta.com",
    "dnb.com",
    "patch.com",
    "bizjournals.com",
    "voyage",
    "canvasrebel",
    "medium.com",
    "youtube.com",
    "about.me",
    "linktr.ee",
    "beacons.ai",
    "substack.com"
  ];
  const intents = ["owner", "founder", "CEO", "principal", "operator", "email", "phone", "contact"];
  for (const site of profileSites) {
    for (const intent of intents) {
      for (const candidateName of names) {
        seeds.push(`site:${site} "${candidateName}" "${city}" ${intent}`.trim());
        if (ownerName) seeds.push(`site:${site} "${ownerName}" "${candidateName}" ${intent}`.trim());
      }
    }
  }
  return uniqueBy(seeds, (query) => normalizeText(query).toLowerCase());
}

function chunk(items, size) {
  const out = [];
  for (let index = 0; index < items.length; index += size) out.push(items.slice(index, index + size));
  return out;
}

function hostFromUrl(url) {
  try {
    return new URL(normalizeUrl(url)).hostname.replace(/^www\./, "");
  } catch {
    return "";
  }
}

function domainMatchesHost(domain = "", host = "") {
  const cleanDomain = String(domain || "").replace(/^www\./, "").toLowerCase();
  const cleanHost = String(host || "").replace(/^www\./, "").toLowerCase();
  return Boolean(cleanDomain && cleanHost && (cleanHost === cleanDomain || cleanHost.endsWith(`.${cleanDomain}`) || cleanDomain.endsWith(`.${cleanHost}`)));
}

function inferOwnerSearchLocation(lead = {}, enrichment = {}) {
  const address = normalizeText(lead.address || "");
  const fromAddress = address.match(/,\s*([^,]+,\s*[A-Z]{2})(?:\s+\d{5})?,\s*(?:United States|USA)\b/i);
  if (fromAddress?.[1]) return normalizeText(fromAddress[1]);
  const combined = normalizeText(`${enrichment.title || ""} ${enrichment.text || ""}`);
  const fromText = combined.match(/\b([A-Z][A-Za-z .'-]{2,40}),\s*([A-Z]{2})\s+\d{5}\b/);
  if (fromText?.[1] && fromText?.[2]) return normalizeText(`${fromText[1]}, ${fromText[2]}`);
  const city = normalizeText(lead.city || "");
  if (city && !/^[a-z ]+,\s*USA$/i.test(city)) return city;
  const state = stateFromLead(lead);
  return state ? `${state}, USA` : city;
}

function leadSearchLocation(lead = {}) {
  return normalizeText(lead.ownerSearchLocation || inferOwnerSearchLocation(lead));
}

function stateFromLead(lead = {}) {
  const address = normalizeText(lead.address || "");
  const addressState = address.match(/,\s*[^,]+,\s*([A-Z]{2})(?:\s+\d{5})?,\s*(?:United States|USA)\b/i);
  if (addressState?.[1]) return addressState[1].toUpperCase();
  const location = normalizeText(lead.ownerSearchLocation || lead.city || "");
  const stateCode = location.match(/\b([A-Z]{2})\b/);
  if (stateCode?.[1]) return stateCode[1].toUpperCase();
  const longState = location.match(/\b(alabama|alaska|arizona|arkansas|california|colorado|connecticut|delaware|florida|georgia|hawaii|idaho|illinois|indiana|iowa|kansas|kentucky|louisiana|maine|maryland|massachusetts|michigan|minnesota|mississippi|missouri|montana|nebraska|nevada|new hampshire|new jersey|new mexico|new york|north carolina|north dakota|ohio|oklahoma|oregon|pennsylvania|rhode island|south carolina|south dakota|tennessee|texas|utah|vermont|virginia|washington|west virginia|wisconsin|wyoming|district of columbia)\b/i);
  return longState?.[1] ? longState[1] : "";
}

function socialHandlesFromUrls(urls = []) {
  const handles = [];
  for (const raw of urls || []) {
    const url = normalizeEvidenceUrl(raw);
    if (!url || !SOCIAL_RE.test(url)) continue;
    try {
      const parsed = new URL(url);
      const parts = parsed.pathname.split("/").map((part) => part.trim()).filter(Boolean);
      const handle = parts.find((part) => !/^(profile\.php|pages|reel|reels|p|tv|status|posts?)$/i.test(part));
      if (!handle) continue;
      const clean = handle.replace(/^@/, "").replace(/[^a-zA-Z0-9._-]/g, "");
      if (clean.length >= 3 && clean.length <= 40) handles.push(clean);
    } catch {
      continue;
    }
  }
  return uniqueBy(handles, (handle) => handle.toLowerCase()).slice(0, 8);
}

function socialHandleSearchQueries(lead = {}) {
  const handles = arrayOfStrings(lead.seedSocialHandles || []).slice(0, 4);
  const location = leadSearchLocation(lead);
  const names = businessNameVariants(lead.name || "").slice(0, 2);
  return handles.flatMap((handle) => [
    `"${handle}" "${lead.name}" owner`,
    `"${handle}" "${location}" owner`,
    ...names.map((name) => `"${handle}" "${name}"`)
  ]);
}

function normalizeStrategy(strategy = {}, fallback) {
  return {
    pagePaths: arrayOfStrings(strategy.pagePaths).length ? arrayOfStrings(strategy.pagePaths) : fallback.pagePaths,
    searchQueries: arrayOfStrings(strategy.searchQueries).length ? arrayOfStrings(strategy.searchQueries) : fallback.searchQueries,
    profileUrlHints: arrayOfStrings(strategy.profileUrlHints),
    ownerNameHints: arrayOfStrings(strategy.ownerNameHints),
    strategyReason: normalizeText(strategy.strategyReason || fallback.strategyReason)
  };
}

function ownerProfileSearchQueries(lead, owner, strategy = {}) {
  const name = primaryBusinessName(lead.name || "");
  const names = businessNameVariants(lead.name || name);
  const city = leadSearchLocation(lead);
  const ownerName = normalizeText(owner?.name || strategy.ownerNameHints?.[0] || "");
  const emailQueries = seedEmailSearchQueries(lead, { names, city });
  return [
    ...names.flatMap((candidateName) => [
      ownerName && `"${ownerName}" "${candidateName}" email`,
      ownerName && `"${ownerName}" "${candidateName}" phone`,
      ownerName && `site:linkedin.com/in "${ownerName}" "${candidateName}"`,
      ownerName && `"${ownerName}" "${candidateName}" interview`,
      `"${candidateName}" owner LinkedIn ${city}`.trim(),
      `"${candidateName}" owner Facebook ${city}`.trim(),
      `"${candidateName}" owner Instagram ${city}`.trim(),
      `"${candidateName}" founder email ${city}`.trim()
    ]),
    ...socialHandleSearchQueries(lead),
    ...emailQueries
  ].filter(Boolean);
}

function extractOwnerCandidates(text, evidenceUrl) {
  const sentences = text.split(/(?<=[.!?])\s+|\n+/).slice(0, 320);
  const out = [];
  for (const sentence of sentences) {
    const lower = sentence.toLowerCase();
    if (!OWNER_TERMS.some((term) => lower.includes(term))) continue;
    const patterns = [
      /([A-Z][A-Za-z'.-]+(?:\s+[A-Z][A-Za-z'.-]+){1,3})\s*(?:,|-|–)?\s*(owner|founder|co-founder|cofounder|co-owner|coowner|ceo|president|principal|proprietor|managing partner|operator)/gi,
      /(owner|founder|co-founder|cofounder|co-owner|coowner|ceo|president|principal|proprietor|managing partner|operator)\s+(?:is|:|of)?\s*([A-Z][A-Za-z'.-]+(?:\s+[A-Z][A-Za-z'.-]+){1,3})/gi,
      /([A-Z][A-Za-z'.-]+(?:\s+[A-Z][A-Za-z'.-]+){1,3})\s+(?:is|serves as|works as)\s+(?:the\s+)?(owner|founder|co-founder|cofounder|co-owner|coowner|ceo|president|principal|proprietor|managing partner|operator)\b/gi,
      /(?:founded|owned|operated|led)\s+by\s+([A-Z][A-Za-z'.-]+(?:\s+[A-Z][A-Za-z'.-]+){1,3})/gi,
      /(co-owner|coowner|proprietor|owner)\s+([A-Z][A-Za-z'.-]+(?:\s+[A-Z][A-Za-z'.-]+){1,3})/gi
    ];
    for (const pattern of patterns) {
      for (const match of sentence.matchAll(pattern)) {
        const firstIsTitle = OWNER_TERMS.includes(String(match[1] || "").toLowerCase());
        const name = normalizePersonName(firstIsTitle ? match[2] : match[1]);
        const title = normalizeText(firstIsTitle ? match[1] : match[2]);
        if (!isLikelyPersonName(name)) continue;
        out.push({ name, title, evidenceUrl, score: 60, reason: "owner-term-near-name" });
      }
    }
  }
  return uniqueBy(out, (candidate) => candidate.name.toLowerCase()).slice(0, 10);
}

function addSearchResultOwnerCandidates({ lead, candidates, evidence, trace, result = {}, source = "" }) {
  const text = normalizeText(`${result.title || ""}. ${result.snippet || ""}`);
  if (!text) return;
  const page = {
    url: result.url || "",
    title: result.title || "",
    text
  };
  if (!pageRelevantToLead(page, lead)) return;

  const found = extractOwnerCandidates(text, result.url || "");
  if (!found.length) return;
  for (const candidate of found) addCandidate(candidates, { ...candidate, score: Math.max(candidate.score || 0, 45), reason: "owner-search-snippet" });
  const names = found.map((candidate) => candidate.name);
  evidence.push({
    source: `${source}:search-snippet`,
    url: result.url || "",
    title: result.title || "",
    ownerCandidates: names,
    socialLinks: [],
    textSample: text.slice(0, 800)
  });
  if (trace) {
    trace.ownerCandidates = uniqueBy([...(trace.ownerCandidates || []), ...names], (name) => name.toLowerCase()).slice(0, 12);
  }
}

function addCandidate(candidates, candidate) {
  const name = normalizePersonName(candidate.name);
  if (!isLikelyPersonName(name)) return;
  const key = name.toLowerCase();
  const current = candidates.get(key) || { name, score: 0, title: "", evidenceUrl: "", reasons: [] };
  current.score += Number(candidate.score || 40);
  current.title ||= normalizeText(candidate.title || "");
  current.evidenceUrl ||= candidate.evidenceUrl || "";
  current.reasons.push(candidate.reason || "owner-evidence");
  candidates.set(key, current);
}

function bestCandidate(candidates) {
  return [...candidates.values()].sort((a, b) => b.score - a.score)[0] || null;
}

function selectOwnerProfiles(urls, owner, lead) {
  const ownerTokens = tokenSet(owner?.name || "");
  const businessTokens = relevantBusinessTokens(lead?.name || "");
  const scored = [];
  for (const raw of urls) {
    const url = normalizeEvidenceUrl(raw);
    if (!url || LINKEDIN_COMPANY_RE.test(url)) continue;
    if (isStaticAssetUrl(url)) continue;
    const lower = decodeURIComponent(url).toLowerCase();
    let score = 0;
    if (LINKEDIN_PROFILE_RE.test(lower)) score += 90;
    else if (SOCIAL_RE.test(lower)) score += 35;
    else if (PUBLIC_EVIDENCE_RE.test(lower)) score += 18;
    for (const token of ownerTokens) {
      if (token.length > 2 && lower.includes(token)) score += 24;
    }
    for (const token of businessTokens) {
      if (token.length > 2 && lower.includes(token)) score += SOCIAL_RE.test(lower) && !owner?.name ? -18 : 8;
    }
    const ownerTokenHits = [...ownerTokens].filter((token) => token.length > 2 && lower.includes(token)).length;
    const businessTokenHits = [...businessTokens].filter((token) => lower.includes(token)).length;
    const ownerRequiredHits = Math.min(2, ownerTokens.size);
    const businessRelevant = profileUrlRelevantToLead(lower, lead, businessTokens, businessTokenHits);
    if (owner?.name && ownerRequiredHits && ownerTokenHits < ownerRequiredHits && !businessRelevant) continue;
    if (owner?.name && SOCIAL_RE.test(lower) && ownerTokenHits < Math.min(2, ownerTokens.size)) continue;
    if (!owner?.name && !businessRelevant) continue;
    if (LINKEDIN_PROFILE_RE.test(lower)) {
      if (owner?.name && ownerTokenHits < Math.min(2, ownerTokens.size)) continue;
      if (!owner?.name && businessTokenHits < Math.min(2, businessTokens.size)) continue;
    }
    if (!owner?.name && !LINKEDIN_PROFILE_RE.test(lower) && SOCIAL_RE.test(lower)) score -= 25;
    if (score > 20) scored.push({ url, score });
  }
  return uniqueBy(scored.sort((a, b) => b.score - a.score), (entry) => entry.url).map((entry) => entry.url);
}

function selectBusinessSocialUrls(urls, lead = {}) {
  const businessTokens = relevantBusinessTokens(lead.name || "");
  const slug = normalizeText(lead.name || "").toLowerCase().replace(/[^a-z0-9]+/g, "");
  const scored = [];
  for (const raw of urls || []) {
    const url = normalizeEvidenceUrl(raw);
    if (!url || !SOCIAL_RE.test(url) || LINKEDIN_COMPANY_RE.test(url) || LINKEDIN_PROFILE_RE.test(url)) continue;
    if (isStaticAssetUrl(url)) continue;
    const lower = decodeURIComponent(url).toLowerCase();
    const compact = lower.replace(/[^a-z0-9]+/g, "");
    let score = 25;
    if (slug && compact.includes(slug)) score += 40;
    for (const token of businessTokens) {
      if (token.length > 2 && lower.includes(token)) score += 12;
    }
    if (/facebook\.com\/profile\.php/i.test(lower)) score += 12;
    if (/instagram\.com\/[a-z0-9._-]+/i.test(lower)) score += 12;
    if (score >= 30) scored.push({ url, score });
  }
  return uniqueBy(scored.sort((a, b) => b.score - a.score), (entry) => entry.url).map((entry) => entry.url);
}

function pageRelevantToLead(page = {}, lead = {}) {
  if (!lead?.name) return true;
  const pageHost = hostFromUrl(page.url || "");
  const leadHost = hostFromUrl(lead.website || "");
  if (pageHost && leadHost && pageHost === leadHost) return true;
  const text = normalizeText(`${page.url || ""} ${page.title || ""} ${page.text || ""}`).toLowerCase();
  const businessName = normalizeText(lead.name).toLowerCase();
  if (businessName && text.includes(businessName)) return true;
  const tokens = relevantBusinessTokens(lead.name);
  if (!tokens.size) return true;
  const hits = [...tokens].filter((token) => text.includes(token)).length;
  if (tokens.size < 2) return false;
  return hits >= 2;
}

function relevantBusinessTokens(value) {
  const ignored = new Set(["the", "and", "llc", "inc", "co", "company", "salon", "spa", "hair", "kids", "kid", "austin", "mesa", "tx", "usa"]);
  return new Set(
    normalizeText(value)
      .toLowerCase()
      .split(/[^a-z0-9]+/)
      .filter((token) => token.length > 3 && !ignored.has(token))
  );
}

function primaryBusinessName(value) {
  return businessNameVariants(value)[0] || normalizeText(value || "");
}

function businessNameVariants(value) {
  const original = normalizeText(value || "");
  if (!original) return [];
  const withoutEmoji = original.replace(/[^\p{L}\p{N}&' .|:()/-]/gu, " ");
  const splitBase = withoutEmoji
    .split(/\s+(?:\||–|—|-|:)\s+/)[0]
    .replace(/\s*\([^)]*(?:off|discount|best|near|open|booking|appointments?)[^)]*\)\s*/gi, " ")
    .trim();
  const marketingStripped = splitBase
    .replace(/\b(?:best|top|premier|premium|luxury|professional)\b.*$/i, "")
    .replace(/\b(?:in|near)\s+(?:atx|austin|round rock|cedar park|pflugerville|georgetown|mesa|phoenix|dallas|houston|san antonio)\b.*$/i, "")
    .replace(/\b(?:central|north|south|east|west)\s+(?:austin|atx|mesa|phoenix|dallas|houston)\b.*$/i, "")
    .trim();
  const compactSuffixStripped = marketingStripped
    .replace(/\b(?:atx|usa|tx|az|ca|fl|ny)\b$/i, "")
    .trim();
  return uniqueBy([
    compactSuffixStripped,
    marketingStripped,
    splitBase,
    original
  ].map((name) => normalizeText(name).replace(/\s+/g, " ").trim()).filter((name) => name.length >= 3), (name) => name.toLowerCase()).slice(0, 4);
}

function profileUrlRelevantToLead(lowerUrl, lead = {}, businessTokens = new Set(), businessTokenHits = 0) {
  const businessName = normalizeText(lead.name || "").toLowerCase();
  const slug = businessName.replace(/[^a-z0-9]+/g, "");
  const compactUrl = String(lowerUrl || "").replace(/[^a-z0-9]+/g, "");
  if (slug && compactUrl.includes(slug)) return true;
  if (!businessTokens.size) return true;
  if (businessTokens.size < 2) return false;
  return businessTokenHits >= 2;
}

function strategyOwnerNameHints(strategy = {}) {
  const explicit = arrayOfStrings(strategy.ownerNameHints).filter(isLikelyPersonName);
  const fromQueries = [];
  for (const query of arrayOfStrings(strategy.searchQueries)) {
    const cleaned = query.replace(/site:[^\s]+/gi, " ");
    for (const match of cleaned.matchAll(/"([A-Z][A-Za-z'.-]+(?:\s+[A-Z][A-Za-z'.-]+){1,3})"/g)) {
      if (isLikelyPersonName(match[1])) fromQueries.push(normalizePersonName(match[1]));
    }
  }
  return uniqueBy([...explicit, ...fromQueries], (name) => name.toLowerCase()).slice(0, 8);
}

function isBusinessOwnedPage(page = {}, lead = {}) {
  const pageHost = hostFromUrl(page.url || "");
  const leadHost = hostFromUrl(lead.website || "");
  return Boolean(pageHost && leadHost && pageHost === leadHost);
}

function emailLocalPartMatchesName(email, name) {
  const local = String(email || "").split("@")[0].toLowerCase().replace(/[^a-z]/g, "");
  if (local.length < 5) return false;
  const parts = normalizePersonName(name).toLowerCase().split(/\s+/).filter(Boolean);
  if (parts.length < 2) return false;
  const first = parts[0].replace(/[^a-z]/g, "");
  const last = parts[parts.length - 1].replace(/[^a-z]/g, "");
  if (first.length < 2 || last.length < 2) return false;
  return local.includes(`${first}${last}`) ||
    local.includes(`${last}${first}`) ||
    (local.includes(first) && local.includes(last));
}

function seedEmailSearchQueries(lead = {}, { name = primaryBusinessName(lead.name || ""), names, city = normalizeText(lead.city || "") } = {}) {
  const searchNames = uniqueBy(arrayOfStrings(names).length ? arrayOfStrings(names) : businessNameVariants(name || lead.name), (item) => item.toLowerCase()).slice(0, 3);
  const emails = uniqueBy([lead.businessEmail, lead.email, ...(lead.emails || [])].filter(isUsableEmail), (email) => String(email).toLowerCase())
    .filter((email) => !isGenericBusinessEmail(email))
    .slice(0, 4);
  const queries = [];
  for (const email of emails) {
    const local = String(email).split("@")[0].toLowerCase();
    const alphaLocal = local.replace(/[^a-z]+/g, " ").trim();
    const compactLocal = local.replace(/[^a-z0-9]+/g, "");
    for (const searchName of searchNames) {
      queries.push(`"${email}" "${searchName}" owner`.trim());
      queries.push(`"${email}" "${searchName}"`.trim());
      if (compactLocal.length >= 5) queries.push(`"${compactLocal}" "${searchName}" owner`.trim());
      if (alphaLocal.length >= 5 && alphaLocal !== compactLocal) queries.push(`"${alphaLocal}" "${searchName}" owner ${city}`.trim());
    }
  }
  return uniqueBy(queries, (query) => normalizeText(query).toLowerCase()).slice(0, 12);
}

function isGenericBusinessEmail(email) {
  const local = String(email || "").split("@")[0].toLowerCase().replace(/[^a-z0-9]+/g, "");
  return /^(info|contact|hello|support|sales|office|admin|appointments?|booking|book|service|services|customerservice|team|frontdesk|reception|careers?|jobs|marketing|editor|webmaster|developer|designer|wordpress|nails?|salon|spa)/.test(local);
}

function personNameLooksLikeBusiness(name, lead = {}) {
  const personTokens = new Set(
    normalizePersonName(name)
      .toLowerCase()
      .split(/\s+/)
      .filter(Boolean)
  );
  if (!personTokens.size) return true;
  const businessTokens = new Set(
    normalizeText([lead.name, lead.category].filter(Boolean).join(" "))
      .toLowerCase()
      .split(/[^a-z0-9]+/)
      .filter(Boolean)
  );
  const overlap = [...personTokens].filter((token) => businessTokens.has(token)).length;
  return overlap >= Math.min(personTokens.size, 2);
}

function isStaticAssetUrl(url) {
  const text = String(url || "").toLowerCase();
  return /(?:\/_nuxt\/|\/assets\/|\/static\/|\/build\/|\/dist\/|\/chunks?\/|\/_next\/|\/cdn-cgi\/)/i.test(text) ||
    /\.(?:css|js|mjs|map|json|png|jpe?g|gif|svg|webp|ico|woff2?|ttf|eot)(?:[?#].*)?$/i.test(text);
}

function isOwnerContactEvidence({ page, owner, lead, source = "", trace }) {
  if (!owner?.name) return false;
  if (personNameLooksLikeBusiness(owner.name, lead)) return false;
  const text = `${page.url || ""} ${page.title || ""} ${page.text || ""}`.toLowerCase();
  const ownerTokens = [...tokenSet(owner.name)].filter((token) => token.length > 2);
  const businessTokens = [...tokenSet(lead?.name || "")].filter((token) => token.length > 2);
  const hasOwner = ownerTokens.length && ownerTokens.every((token) => text.includes(token));
  const hasBusiness = businessTokens.some((token) => text.includes(token)) || hostFromUrl(lead?.website) && text.includes(hostFromUrl(lead.website));
  const isOwnerPass = /business-rabbit-hole|profile-contact-swarm|direct-owner-contact|linkedin-person-profile|owner-profile-search|owner-identity|planned/i.test(`${source} ${trace?.role || ""} ${trace?.planner || ""}`);
  const profileLike = LINKEDIN_PROFILE_RE.test(page.url || "") || PUBLIC_EVIDENCE_RE.test(page.url || "");
  if (!hasOwner || !hasBusiness) return false;
  const directoryLike = isDirectoryEvidenceUrl(page.url || "");
  if (contactNearOwner(page, owner)) return !directoryLike && (explicitDirectOwnerContact(page, owner) || !/business-rabbit-hole|business-home|owner-profile-fetch/i.test(source));
  const directProfilePass = /profile-contact-swarm|direct-owner-contact|linkedin-person-profile|owner-profile-fetch|owner-profile-search|planned/i.test(`${source} ${trace?.role || ""} ${trace?.planner || ""}`);
  return Boolean((directProfilePass || !isOwnerPass) && profileLike && !directoryLike);
}

function explicitDirectOwnerContact(page = {}, owner = {}) {
  const name = normalizePersonName(owner.name || "");
  if (!name) return false;
  const first = name.split(/\s+/)[0] || "";
  const text = normalizeText(`${page.title || ""} ${page.text || ""}`).toLowerCase();
  const escapedName = escapeRegExp(name.toLowerCase());
  const escapedFirst = escapeRegExp(first.toLowerCase());
  return new RegExp(`\\b(contact|call|email|reach|text)\\s+(owner\\s+)?(${escapedName}|${escapedFirst})\\b`).test(text) ||
    new RegExp(`\\b${escapedName}\\b.{0,80}\\b(direct|directly|personal|cell|mobile)\\b`).test(text);
}

function isDirectoryEvidenceUrl(url = "") {
  const host = hostFromUrl(url);
  return /\b(bbb|yelp|angi|angieslist|homeadvisor|thumbtack|mapquest|yellowpages|manta|chamberofcommerce|dnb|opencorporates|bizapedia)\./i.test(host);
}

function escapeRegExp(value = "") {
  return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

function recordOwnerContacts({ ownerEmails, ownerPhones, ownerContactEvidence, page = {}, owner, lead, emails = [], phones = [], source = "", reason = "" }) {
  if (!owner?.name || personNameLooksLikeBusiness(owner.name, lead)) return;
  if (isDirectoryEvidenceUrl(page.url || "")) return;
  for (const email of emails || []) {
    const value = String(email || "").trim().toLowerCase();
    if (!isUsableEmail(value)) continue;
    ownerEmails?.add(value);
    addOwnerContactEvidence(ownerContactEvidence, {
      type: "email",
      value,
      page,
      owner,
      lead,
      source,
      reason
    });
  }

  for (const phone of phones || []) {
    const value = normalizePhone(phone);
    if (!value) continue;
    ownerPhones?.add(value);
    addOwnerContactEvidence(ownerContactEvidence, {
      type: "phone",
      value,
      page,
      owner,
      lead,
      source,
      reason
    });
  }
}

function promoteHintContactsFromOwnedPage({ strategy = {}, page = {}, lead = {}, candidates, ownerEmails, ownerPhones, ownerContactEvidence }) {
  if (!isBusinessOwnedPage(page, lead)) return;
  const hints = strategyOwnerNameHints(strategy);
  if (!hints.length) return;

  for (const hint of hints) {
    if (!isLikelyPersonName(hint)) continue;
    if (personNameLooksLikeBusiness(hint, lead)) continue;
    const owner = {
      name: normalizePersonName(hint),
      title: "Owner",
      evidenceUrl: page.url || lead.website || "",
      score: 62,
      reason: "owner-name-hint-matched-business-contact"
    };
    const matchedEmails = (page.emails || []).filter((email) => isUsableEmail(email) && emailLocalPartMatchesName(email, owner.name));
    const matchedPhones = explicitDirectOwnerContact(page, owner) ? (page.phones || []) : [];
    if (!matchedEmails.length && !matchedPhones.length) continue;
    addCandidate(candidates, owner);
    recordOwnerContacts({
      ownerEmails,
      ownerPhones,
      ownerContactEvidence,
      page,
      owner,
      lead,
      emails: matchedEmails,
      phones: matchedPhones,
      source: "owner-name-hint-business-page",
      reason: "business-owned page contact matches a corroborated owner name hint"
    });
  }
}

function promoteKnownOwnerContactsFromOwnedPages({ pages = [], lead = {}, owner, ownerEmails, ownerPhones, ownerContactEvidence }) {
  if (!owner?.name || personNameLooksLikeBusiness(owner.name, lead)) return;
  for (const page of pages || []) {
    if (!isBusinessOwnedPage(page, lead)) continue;
    const matchedEmails = (page.emails || []).filter((email) => isUsableEmail(email) && emailLocalPartMatchesName(email, owner.name));
    const matchedPhones = explicitDirectOwnerContact(page, owner) ? (page.phones || []) : [];
    if (!matchedEmails.length && !matchedPhones.length) continue;
    recordOwnerContacts({
      ownerEmails,
      ownerPhones,
      ownerContactEvidence,
      page,
      owner,
      lead,
      emails: matchedEmails,
      phones: matchedPhones,
      source: "known-owner-business-page",
      reason: "business-owned page contact matches independently discovered owner identity"
    });
  }
}

function addOwnerContactEvidence(ownerContactEvidence, { type, value, page = {}, owner, lead, source = "", reason = "" }) {
  if (!Array.isArray(ownerContactEvidence) || !value) return;
  if (!owner?.name || personNameLooksLikeBusiness(owner.name, lead)) return;
  const key = `${type}:${value}:${page.url || ""}`;
  if (ownerContactEvidence.some((entry) => `${entry.type}:${entry.value}:${entry.url}` === key)) return;
  ownerContactEvidence.push({
    type,
    value,
    url: page.url || "",
    title: page.title || "",
    source,
    ownerName: owner?.name || "",
    businessName: lead?.name || "",
    reason,
    toolchain: Array.isArray(page.toolchain) ? page.toolchain : [],
    toolAttempts: Array.isArray(page.toolAttempts) ? page.toolAttempts.map((attempt) => ({
      tool: attempt.tool,
      status: attempt.status,
      extracted: attempt.extracted || {}
    })) : []
  });
}

function ownerEvidenceHasPersonName(entry = {}, lead = {}) {
  const name = normalizePersonName(entry.ownerName || "");
  return Boolean(name && isLikelyPersonName(name) && !personNameLooksLikeBusiness(name, lead));
}

function shouldFetchOwnerSearchResult({ source = "", trace, result = {} }) {
  const text = `${source} ${trace?.role || ""} ${trace?.hypothesis || ""} ${result.title || ""} ${result.snippet || ""}`;
  if (!/owner|founder|ceo|principal|operator|contact|email|phone|linkedin|profile/i.test(text)) return false;
  return /identity-swarm|profile-contact-swarm|direct-owner-contact|linkedin-person-profile|website-leadership|business-directories|business-registry|articles-interviews|planned|owner-identity|adaptive-rabbit-hole/i.test(text);
}

function contactNearOwner(page = {}, owner = {}) {
  const text = normalizeText(page.text || "");
  if (!text || !owner?.name) return false;
  const lowerText = text.toLowerCase();
  const ownerIndex = lowerText.indexOf(normalizeText(owner.name).toLowerCase());
  if (ownerIndex < 0) return false;
  for (const email of page.emails || []) {
    const value = normalizeText(email).toLowerCase();
    if (!value) continue;
    const index = lowerText.indexOf(value);
    if (index >= 0 && Math.abs(index - ownerIndex) <= 420) return true;
  }
  const phoneMatches = [...text.matchAll(/(?:\+?1[\s().-]*)?(?:\(?\d{3}\)?[\s().-]*)\d{3}[\s().-]*\d{4}/g)];
  for (const phone of page.phones || []) {
    const digits = normalizePhone(phone).replace(/\D/g, "");
    if (digits.length < 7) continue;
    for (const match of phoneMatches) {
      const foundDigits = String(match[0] || "").replace(/\D/g, "");
      if (!foundDigits.endsWith(digits.slice(-10)) && !foundDigits.endsWith(digits.slice(-7))) continue;
      if (Math.abs(Number(match.index || 0) - ownerIndex) <= 420) return true;
    }
  }
  return false;
}

async function searchPublicWeb(query) {
  const duckDuckGo = await searchDuckDuckGo(query);
  if (duckDuckGo.length) return duckDuckGo;
  return searchBing(query);
}

async function searchDuckDuckGo(query) {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), DUCKDUCKGO_TIMEOUT_MS);
  try {
    const url = `https://duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
    const response = await fetch(url, {
      headers: { "user-agent": "HermesOwnerResearch/0.1 (+local workflow)" },
      signal: controller.signal
    });
    if (!response.ok) return [];
    const html = await response.text();
    return parseDuckDuckGoResults(html).slice(0, 8);
  } catch {
    return [];
  } finally {
    clearTimeout(timeout);
  }
}

async function searchBing(query) {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), BING_TIMEOUT_MS);
  try {
    const url = `https://www.bing.com/search?q=${encodeURIComponent(query)}`;
    const response = await fetch(url, {
      headers: { "user-agent": "Mozilla/5.0 HermesOwnerResearch/0.1" },
      signal: controller.signal
    });
    if (!response.ok) return [];
    const html = await response.text();
    return parseBingResults(html).slice(0, 8);
  } catch {
    return [];
  } finally {
    clearTimeout(timeout);
  }
}

function parseDuckDuckGoResults(html) {
  const out = [];
  const resultRe = /<a[^>]+class="result__a"[^>]+href="([^"]+)"[^>]*>([\s\S]*?)<\/a>[\s\S]{0,800}?<a[^>]+class="result__snippet"[^>]*>([\s\S]*?)<\/a>/gi;
  for (const match of html.matchAll(resultRe)) {
    const url = unwrapDuckDuckGoUrl(decodeHtml(match[1]));
    if (!url) continue;
    out.push({
      url,
      title: stripHtml(match[2]),
      snippet: stripHtml(match[3])
    });
  }
  if (out.length) return uniqueBy(out, (result) => result.url);

  const linkRe = /<a[^>]+href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi;
  for (const match of html.matchAll(linkRe)) {
    const url = unwrapDuckDuckGoUrl(decodeHtml(match[1]));
    if (!url || !PUBLIC_EVIDENCE_RE.test(url)) continue;
    out.push({ url, title: stripHtml(match[2]), snippet: "" });
  }
  return uniqueBy(out, (result) => result.url);
}

function parseBingResults(html) {
  const out = [];
  const resultRe = /<li[^>]+class="b_algo"[\s\S]*?<h2[^>]*>[\s\S]*?<a[^>]+href="([^"]+)"[^>]*>([\s\S]*?)<\/a>[\s\S]*?(?:<p[^>]*>([\s\S]*?)<\/p>)?/gi;
  for (const match of html.matchAll(resultRe)) {
    const url = unwrapBingUrl(decodeHtml(match[1]));
    if (!url) continue;
    out.push({
      url,
      title: stripHtml(match[2]),
      snippet: stripHtml(match[3] || "")
    });
  }
  return uniqueBy(out, (result) => result.url);
}

function unwrapDuckDuckGoUrl(raw) {
  const text = String(raw || "");
  try {
    const url = new URL(text.startsWith("http") ? text : `https://duckduckgo.com${text}`);
    const uddg = url.searchParams.get("uddg");
    return normalizeEvidenceUrl(uddg || text);
  } catch {
    return normalizeEvidenceUrl(text);
  }
}

function unwrapBingUrl(raw) {
  const text = String(raw || "");
  try {
    const url = new URL(text.startsWith("http") ? text : `https://www.bing.com${text}`);
    const encoded = url.searchParams.get("u");
    if (encoded) {
      const payload = encoded.startsWith("a1") ? encoded.slice(2) : encoded;
      const decoded = Buffer.from(payload, "base64").toString("utf8");
      if (decoded.startsWith("http")) return normalizeEvidenceUrl(decoded);
    }
    return normalizeEvidenceUrl(text);
  } catch {
    return normalizeEvidenceUrl(text);
  }
}

function normalizeEvidenceUrl(url) {
  const clean = normalizeUrl(String(url || "").split("#")[0].replace(/\/$/, ""));
  return clean;
}

function tokenSet(value) {
  return new Set(String(value || "").toLowerCase().match(/[a-z0-9]+/g) || []);
}

function normalizePersonName(value) {
  return normalizeText(value)
    .replace(/[^\p{L}' .-]/gu, " ")
    .replace(/\s+/g, " ")
    .trim();
}

function isLikelyPersonName(value) {
  const text = normalizePersonName(value);
  if (!text) return false;
  if (/\b(about|contact|home|services|team|welcome|restaurant|dental|roofing|salon|clinic|company|business|owner|founder|ceo|president|principal|operator|stylist|artist|expert|staff|leadership|value|values|statement|statements|policy|policies|career|careers|product|products|book|booking|gift|cards|service|hair|color|extension|extensions|nail|nails|spa|lounge|texas|best|premier|premium|tour|travel|tourism|vietnam|vietnamese|india|canada|casino|bonus|payouts|fortune|coins|our|co)\b/i.test(text)) return false;
  const parts = text.split(/\s+/);
  if (parts.length < 2 || parts.length > 4) return false;
  return parts.every((part) => {
    if (!/^[A-Z][A-Za-z'.-]*[A-Za-z]$/.test(part)) return false;
    if (!/[a-z]/.test(part.slice(1))) return false;
    return true;
  });
}

function isUsableEmail(email) {
  const text = String(email || "").trim().toLowerCase();
  if (!/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(text)) return false;
  if (text.includes("noreply") || text.includes("example.") || text.includes("sentry.") || text.includes("wixpress.")) return false;
  return true;
}

function arrayOfStrings(value) {
  return Array.isArray(value) ? value.map((item) => normalizeText(item)).filter(Boolean).slice(0, 12) : [];
}

function ownerAgentConcurrency(lead = {}) {
  const requested = Math.max(1, Math.min(50, Number(lead.ownerAgentSwarmSize || process.env.HERMES_OWNER_AGENT_SWARM_SIZE || 20)));
  if (ownerMinimaxTimeoutRateCircuitOpen(lead.run)) return Math.min(requested, 8);
  if (!lead.ownerIdentityKnown) return Math.min(requested, Number(process.env.HERMES_OWNER_IDENTITY_AGENT_CAP || 6));
  return requested;
}

function ownerSearchQueryLimit(run = {}) {
  return ownerMinimaxTimeoutRateCircuitOpen(run) ? Math.min(MAX_SEARCH_QUERIES, 6) : MAX_SEARCH_QUERIES;
}

function ownerSearchResultsPerQuery(run = {}) {
  return ownerMinimaxTimeoutRateCircuitOpen(run) ? Math.min(MAX_SEARCH_RESULTS_PER_QUERY, 2) : MAX_SEARCH_RESULTS_PER_QUERY;
}

async function mapLimit(items, limit, worker) {
  const input = Array.from(items || []);
  if (!input.length) return [];
  const width = Math.max(1, Math.min(Number(limit) || 1, input.length));
  const results = new Array(input.length);
  let index = 0;
  await Promise.all(Array.from({ length: width }, async () => {
    while (index < input.length) {
      const current = index;
      index += 1;
      results[current] = await worker(input[current], current);
    }
  }));
  return results;
}

function leadKeyForStrategy(lead) {
  return normalizeText([lead.name, lead.website, lead.city].filter(Boolean).join("|")).toLowerCase();
}

function stripHtml(value) {
  return decodeHtml(String(value || "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim());
}

function decodeHtml(value) {
  return String(value || "")
    .replace(/&amp;/g, "&")
    .replace(/&quot;/g, '"')
    .replace(/&#39;/g, "'")
    .replace(/&lt;/g, "<")
    .replace(/&gt;/g, ">");
}
