import { ownerFirstFallbackSearchQueries } from "./ownerFirstDiscovery.js";

const DEFAULT_BASE_URL = "https://api.minimax.io/v1";
const DEFAULT_MODEL = "MiniMax-M3";
const DEFAULT_TIMEOUT_MS = 90000;
const DEFAULT_RETRIES = 2;
const DEFAULT_RETRY_DELAY_MS = 1500;
const DEFAULT_MAX_CONCURRENCY = 3;
const DEFAULT_RPM_LIMIT = 120;
const RATE_WINDOW_MS = 60_000;

export class MiniMaxClient {
  constructor() {
    this.apiKey = process.env.MINIMAX_API_KEY;
    this.baseUrl = (process.env.MINIMAX_BASE_URL || DEFAULT_BASE_URL).replace(/\/$/, "");
    this.model = process.env.MINIMAX_MODEL || DEFAULT_MODEL;
    this.timeoutMs = Math.max(1000, Number(process.env.HERMES_MINIMAX_TIMEOUT_MS || DEFAULT_TIMEOUT_MS));
    this.retries = Math.max(0, Number(process.env.HERMES_MINIMAX_RETRIES || DEFAULT_RETRIES));
    this.retryDelayMs = Math.max(0, Number(process.env.HERMES_MINIMAX_RETRY_DELAY_MS || DEFAULT_RETRY_DELAY_MS));
    this.scheduler = new MiniMaxRequestScheduler({
      maxConcurrency: Number(process.env.HERMES_MINIMAX_MAX_CONCURRENCY || DEFAULT_MAX_CONCURRENCY),
      rpmLimit: Number(process.env.HERMES_MINIMAX_RPM_LIMIT || DEFAULT_RPM_LIMIT)
    });
  }

  configured() {
    return Boolean(this.apiKey);
  }

  metrics() {
    return this.scheduler.snapshot();
  }

  cancelPending(reason = "MiniMax queue cancelled.") {
    return this.scheduler.cancelPending(reason);
  }

  async chatJson({ system, user, fallback, timeoutMs }) {
    if (!this.configured()) return fallback;

    const requestTimeoutMs = Math.max(1000, Number(timeoutMs || this.timeoutMs));
    const startedAt = Date.now();
    let timeoutCount = 0;
    const providerMeta = (extra = {}) => ({
      provider: "minimax",
      model: this.model,
      durationMs: Date.now() - startedAt,
      timeoutMs: requestTimeoutMs,
      attempts: extra.attempts || 1,
      timeoutCount,
      scheduler: this.scheduler.snapshot(),
      ...extra
    });

    for (let attempt = 1; attempt <= this.retries + 1; attempt += 1) {
      const controller = new AbortController();
      const timeout = setTimeout(() => controller.abort(), requestTimeoutMs);
      try {
        const response = await this.scheduler.run(() => fetch(`${this.baseUrl}/chat/completions`, {
          method: "POST",
          headers: {
            "authorization": `Bearer ${this.apiKey}`,
            "content-type": "application/json"
          },
          signal: controller.signal,
          body: JSON.stringify({
            model: this.model,
            thinking: { type: "disabled" },
            temperature: 0.2,
            messages: [
              { role: "system", content: `${system}\nReturn compact JSON only. Do not include markdown.` },
              { role: "user", content: user }
            ]
          })
        }));

        if (!response.ok) {
          const detail = await response.text().catch(() => "");
          const providerError = parseProviderError(detail);
          const retryable = isRetryableProviderFailure(response.status, providerError.code);
          if (retryable && attempt <= this.retries) {
            this.scheduler.cooldown(providerBackoffMs({
              retryAfter: response.headers.get("retry-after"),
              code: providerError.code,
              attempt
            }));
            await delay(retryDelayForAttempt(this.retryDelayMs, attempt, response.headers.get("retry-after")));
            continue;
          }
          return withProviderFallback(fallback, `MiniMax API error ${response.status}: ${providerError.message || detail.slice(0, 240)}`, {
            ...providerMeta({ attempts: attempt }),
            status: response.status,
            providerCode: providerError.code
          });
        }

      const data = await response.json();
        const content = messageContentToText(data.choices?.[0]?.message?.content);
        const parsed = parseJsonFromText(content);
        if (!parsed) {
          if (attempt <= this.retries) {
            await delay(retryDelayForAttempt(this.retryDelayMs, attempt));
            continue;
          }
          return withProviderFallback(fallback, "MiniMax returned non-JSON content.", providerMeta({ attempts: attempt }));
        }
        return withProviderMeta(parsed, providerMeta({ attempts: attempt }));
      } catch (error) {
        const timedOut = error?.name === "AbortError";
        if (timedOut) timeoutCount += 1;
        if ((timedOut || isRetryableNetworkError(error)) && attempt <= this.retries) {
          if (timedOut) this.scheduler.cooldown(retryDelayForAttempt(this.retryDelayMs, attempt));
          await delay(retryDelayForAttempt(this.retryDelayMs, attempt));
          continue;
        }
        return withProviderFallback(fallback, timedOut ? `MiniMax request timed out after ${requestTimeoutMs}ms.` : error.message || String(error), {
          ...providerMeta({ attempts: attempt }),
          timedOut
        });
      } finally {
        clearTimeout(timeout);
      }
    }

    return fallback;
  }

  async qualifyLead({ lead, pageText, criteria }) {
    const countryScope = normalizeCountryScope(lead.country || "United States");
    const nonTargetLead = hasExplicitDifferentCountry(`${lead.address || ""} ${lead.city || ""} ${lead.country || ""}`, countryScope);
    const ownerMode = lead.contactTarget === "business_owner";
    const reachable = ownerMode
      ? Boolean(lead.ownerName && (lead.ownerEmail || lead.ownerPhone))
      : Boolean(lead.phone || lead.email || lead.website);
    const fallback = {
      qualified: !nonTargetLead && reachable,
      score: nonTargetLead ? 0 : reachable ? ownerMode && lead.ownerEmail && lead.ownerPhone ? 100 : 90 : 0,
      reason: nonTargetLead
        ? `Rejected by ${countryScope}-only fallback qualification.`
        : ownerMode
          ? "Owner-contact heuristic qualification: owner identity plus owner email or phone found."
          : `${countryScope}-only heuristic qualification: lead has at least one reachable contact channel.`,
      missing: nonTargetLead
        ? [`${countryScope} location`]
        : reachable
          ? []
          : ownerMode
            ? ["business owner identity and owner email or owner phone"]
            : ["reachable website, email, or phone"]
    };

    return this.chatJson({
      fallback,
      system: `You are Hermes, a strict B2B lead qualification agent. Score only based on the provided data. The workflow is locked to ${countryScope}; reject any lead that is not physically located in ${countryScope}. For business-owner campaigns, Google Maps and business website contact are seed evidence only and must not satisfy final qualification. Final qualification requires a named business owner plus owner-level email or owner-level phone found from public owner evidence such as social/profile pages. Do not guess owner names, emails, or phones.`,
      user: JSON.stringify({
        task: ownerMode
          ? "Decide whether this business has a qualified owner contact lead."
          : `Decide whether this business is a qualified ${countryScope} target lead.`,
        countryScope: `${countryScope} only`,
        criteria,
        lead,
        pageText: String(pageText || "").slice(0, 7000),
        scoringRubric: {
          "100": ownerMode
            ? "Owner name and both owner email and owner phone are found from owner-level evidence."
            : "All requested hard criteria are satisfied by the available evidence.",
          "70-99": ownerMode
            ? "Owner name and one owner contact channel are found from owner-level evidence."
            : "Likely match, but one requested hard criterion is uncertain or weakly evidenced.",
          "1-69": "Partial match with important missing required evidence.",
          "0": ownerMode
            ? "No named owner, no owner-level contact, wrong niche/location, or contact is only business-level."
            : `Not a ${countryScope} lead, wrong niche/location, or no reachable contact channel.`
        },
        outputSchema: {
          qualified: "boolean",
          score: "0-100 number",
          reason: "short string",
          missing: ["missing required fields"]
        }
      })
    });
  }

  async proposeOwnerRabbitHoleStrategy({ lead, evidence, priorAttempts = [], timeoutMs }) {
    const fallback = fallbackOwnerRabbitHoleStrategy(lead);
    return this.chatJson({
      fallback,
      timeoutMs,
      system:
        "You are Hermes' owner-contact research strategist. Build a public-web rabbit-hole plan for finding the owner of one local business and then finding owner-level contact information. " +
        "Use Google Maps/business website details only as seed evidence. Do not guess owner names or contact info. Prefer public social/profile pages, interviews, articles, directories, and leadership pages that can prove owner identity.",
      user: JSON.stringify({
        task: "Create a search and scraping strategy for this one business-owner contact lead.",
        business: {
          name: lead.name,
          website: lead.website,
          city: lead.city,
          address: lead.address,
          category: lead.category,
          businessPhoneSeed: lead.businessPhone || lead.phone || "",
          businessEmailSeed: lead.businessEmail || lead.email || ""
        },
        evidence: (evidence || []).slice(0, 8),
        priorAttempts,
        rules: [
          "Return only public URLs and public search queries.",
          "Do not use business-level contact as final owner contact.",
          "Look for owner/founder/CEO/principal/operator names.",
          "Look for LinkedIn personal profiles first, then Facebook/Instagram/X, then interviews/articles/directories.",
          "Prefer exact business name plus city to avoid false matches."
        ],
        outputSchema: {
          pagePaths: ["business website paths to inspect, such as /about-us"],
          searchQueries: ["public web search queries"],
          profileUrlHints: ["specific public profile/article URLs if already visible in evidence"],
          ownerNameHints: ["possible owner names only if evidence supports them"],
          strategyReason: "short reason"
        }
      })
    });
  }

  async proposeOwnerAgentSwarm({ lead, owner, strategy, evidence, phase, requestedAgents, timeoutMs }) {
    const fallback = { agents: [], reason: "Fallback templates will generate the owner-agent swarm." };
    return this.chatJson({
      fallback,
      timeoutMs,
      system:
        "You are Hermes' multi-agent owner research orchestrator. Split one business-owner investigation into separate public-web agents. " +
        "Each agent must think through a different plausible path to prove the owner identity, find a personal LinkedIn/profile URL, or find owner-level email/phone. " +
        "Do not invent owner names, emails, phones, or URLs. Return only public search queries and public URL hints.",
      user: JSON.stringify({
        task: "Design independent rabbit-hole agents for this one business-owner lead.",
        phase,
        requestedAgents,
        business: {
          name: lead.name,
          website: lead.website,
          city: lead.city,
          address: lead.address,
          category: lead.category,
          businessPhoneSeed: lead.businessPhone || lead.phone || "",
          businessEmailSeed: lead.businessEmail || lead.email || ""
        },
        currentOwnerCandidate: owner ? {
          name: owner.name,
          title: owner.title,
          evidenceUrl: owner.evidenceUrl,
          reasons: owner.reasons
        } : null,
        strategy,
        evidence: (evidence || []).slice(0, 10),
        rules: [
          "Return up to requestedAgents agents.",
          "Every agent must have a unique angle and 2-5 concrete public search queries.",
          "Prioritize personal LinkedIn profile discovery when owner identity is known or hinted.",
          "Include non-LinkedIn routes too: website leadership pages, local press, interviews, directories, social profiles, and direct owner contact searches.",
          "Do not use business phone/email as final owner contact evidence.",
          "Prefer exact business name plus city to avoid false owner matches."
        ],
        outputSchema: {
          agents: [{
            role: "short unique role id",
            hypothesis: "what this agent is trying to prove",
            queries: ["public search queries"],
            priority: "1-100 number"
          }],
          reason: "short reason"
        }
      })
    });
  }

  async proposeRetry({ failure, context }) {
    const countryScope = normalizeCountryScope(context.countryScope || "United States");
    const fallbackQuery = Array.isArray(context.untriedQueryHints) && context.untriedQueryHints.length
      ? context.untriedQueryHints[0]
      : `${context.niche} ${context.city} official website phone ${countryScope}`;

    return this.chatJson({
      fallback: {
        variable: "query",
        value: fallbackQuery,
        reason: `Fallback retry uses an unused ${countryScope} query form instead of repeating the failed search.`
      },
      system: `You improve lead discovery workflows by changing exactly one variable at a time. The workflow is locked to ${countryScope}; never propose a query or location outside ${countryScope}. Use operationalMemory.skills as preferred repairs and operationalMemory.avoid as constraints. Do not repeat any attempted query. If the search found duplicates or too few leads, broaden or vary the query while preserving the niche and target location.`,
      user: JSON.stringify({
        task: `Propose one retry change for a ${countryScope}-only lead search. Only change one of: query, location_split, fetch_mode, timeout, page_list, selector.`,
        countryScope: `${countryScope} only`,
        failure,
        context,
        outputSchema: {
          variable: "one allowed variable",
          value: "new value",
          reason: "short reason"
        }
      })
    });
  }

  async proposeSkill({ success, failedStep }) {
    return this.chatJson({
      fallback: {
        title: "Retry pattern worked",
        when: failedStep?.classification || "recoverable failure",
        do: success?.change || "repeat the successful retry",
        avoid: failedStep?.avoid || "rerunning the same failed settings"
      },
      system: "You convert successful workflow repairs into reusable operational skills.",
      user: JSON.stringify({
        task: "Create one concise Hermes skill from this successful retry.",
        success,
        failedStep,
        outputSchema: {
          title: "short string",
          when: "condition",
          do: "action",
          avoid: "negative lesson"
        }
      })
    });
  }

  async proposeSearchStrategy({ context, timeoutMs }) {
    const countryScope = normalizeCountryScope(context.countryScope || "United States");
    const fallback = {
      terms: [],
      queries: fallbackSearchQueries(context),
      reason: "Fallback strategy broadens local search intent without repeating attempted queries."
    };

    return this.chatJson({
      fallback,
      timeoutMs,
      system: `You are Hermes' owner-first search strategist. Do not merely search for businesses matching the broad keyword. Generate ${countryScope}-only discovery queries for situations where independent owner-operated businesses reveal owners: openings, ribbon cuttings, founder stories, owner-operated phrases, local news, licenses, permits, chambers, community events, and ownership transitions. Avoid chain/franchise/corporate-heavy inventory searches. Return only practical search strings, not explanations.`,
      user: JSON.stringify({
        task: "Create adaptive search queries for the next lead discovery attempt.",
        countryScope: `${countryScope} only`,
        context,
        rules: [
          "Do not repeat attemptedQueries.",
          `Keep every query in the same ${countryScope} target area unless a new location is explicitly requested.`,
          "Treat the niche as a market category, then search owner-visible angles rather than broad inventory.",
          "Use niche-specific owner archetypes, source families, lifecycle events, and independent-owner signals.",
          "Prefer queries likely to expose named owners before owner-evidence work: owner-operated, founder, co-owner, family-owned, opened by, under new ownership, ribbon cutting, chamber, local press, license, permit, our story.",
          "Avoid generic near me, best, directory, email phone, and corporate-location searches unless they are a final fallback.",
          "Do not weaken the user's qualification criteria."
        ],
        outputSchema: {
          terms: ["short niche synonyms or subtypes"],
          queries: ["complete Google Maps search query strings"],
          reason: "short reason"
        }
      })
    });
  }

  async proposeLocationStrategy({ context }) {
    const countryScope = normalizeCountryScope(context.countryScope || "United States");
    const fallback = {
      locations: Array.isArray(context.untriedLocationHints) ? context.untriedLocationHints.slice(0, 8) : [],
      reason: `Fallback location strategy expands into nearby or related ${countryScope} markets because the current plan is under target.`
    };

    return this.chatJson({
      fallback,
      system: `You are Hermes' ${countryScope}-only territory planner. Add practical nearby or related ${countryScope} locations when the current lead plan is under target. Use operationalMemory when present. Do not propose locations outside ${countryScope} and do not repeat planned locations.`,
      user: JSON.stringify({
        task: `Propose additional ${countryScope} search locations for lead discovery.`,
        countryScope: `${countryScope} only`,
        context,
        rules: [
          `Return city or city/region strings inside ${countryScope}.`,
          "Do not repeat plannedLocations or exhaustedLocations.",
          "Prefer nearby metro areas, high-population cities, and adjacent markets.",
          `Preserve the user intent and do not move outside ${countryScope}.`
        ],
        outputSchema: {
          locations: ["city/state strings"],
          reason: "short reason"
        }
      })
    });
  }

  async proposeRunReview({ run }) {
    const fallback = fallbackRunReview(run);
    return this.chatJson({
      fallback,
      system: "You are Hermes' self-review agent. Convert completed lead-generation runs into reusable operational memory. Extract concrete skills that should be reused and avoid lessons that prevent repeated failure. Keep every lesson country-targeted and operational.",
      user: JSON.stringify({
        task: "Review this run and produce reusable memory.",
        run,
        outputSchema: {
          summary: "short run diagnosis",
          skills: [{ title: "short string", when: "condition", do: "action", avoid: "negative lesson" }],
          avoid: [{ stage: "stage name", failure: "failure pattern", avoid: "specific prevention" }]
        }
      })
    });
  }
}

class MiniMaxRequestScheduler {
  constructor({ maxConcurrency, rpmLimit }) {
    this.maxConcurrency = Math.max(1, Math.min(20, Number(maxConcurrency) || DEFAULT_MAX_CONCURRENCY));
    this.rpmLimit = Math.max(1, Math.min(500, Number(rpmLimit) || DEFAULT_RPM_LIMIT));
    this.active = 0;
    this.queue = [];
    this.startedAt = [];
    this.cooldownUntil = 0;
    this.totalQueued = 0;
    this.totalStarted = 0;
    this.totalCompleted = 0;
    this.maxPending = 0;
    this.cooldowns = 0;
    this.drainTimer = null;
  }

  run(task) {
    this.totalQueued += 1;
    this.maxPending = Math.max(this.maxPending, this.queue.length + 1);
    return new Promise((resolve, reject) => {
      this.queue.push({ task, resolve, reject });
      this.drain();
    });
  }

  cooldown(ms) {
    const delayMs = Math.max(0, Math.min(120_000, Number(ms) || 0));
    if (!delayMs) return;
    this.cooldowns += 1;
    this.cooldownUntil = Math.max(this.cooldownUntil, Date.now() + delayMs);
    this.drain();
  }

  cancelPending(reason = "MiniMax queue cancelled.") {
    if (this.drainTimer) {
      clearTimeout(this.drainTimer);
      this.drainTimer = null;
    }
    const pending = this.queue.splice(0);
    for (const item of pending) {
      item.reject(new Error(reason));
    }
    return pending.length;
  }

  snapshot() {
    this.prune(Date.now());
    return {
      active: this.active,
      pending: this.queue.length,
      maxConcurrency: this.maxConcurrency,
      rpmLimit: this.rpmLimit,
      requestsInWindow: this.startedAt.length,
      cooldownMs: Math.max(0, this.cooldownUntil - Date.now()),
      totalQueued: this.totalQueued,
      totalStarted: this.totalStarted,
      totalCompleted: this.totalCompleted,
      maxPending: this.maxPending,
      cooldowns: this.cooldowns
    };
  }

  drain() {
    if (this.drainTimer) {
      clearTimeout(this.drainTimer);
      this.drainTimer = null;
    }

    const now = Date.now();
    this.prune(now);
    const waitMs = this.nextWaitMs(now);
    if (waitMs > 0) {
      this.drainTimer = setTimeout(() => {
        this.drainTimer = null;
        this.drain();
      }, waitMs);
      return;
    }

    while (this.queue.length && this.active < this.maxConcurrency && this.startedAt.length < this.rpmLimit) {
      const item = this.queue.shift();
      this.active += 1;
      this.totalStarted += 1;
      this.startedAt.push(Date.now());
      Promise.resolve()
        .then(item.task)
        .then(item.resolve, item.reject)
        .finally(() => {
          this.active -= 1;
          this.totalCompleted += 1;
          this.drain();
        });
    }

    if (this.queue.length) {
      const next = this.nextWaitMs(Date.now());
      if (next > 0) {
        this.drainTimer = setTimeout(() => {
          this.drainTimer = null;
          this.drain();
        }, next);
      }
    }
  }

  nextWaitMs(now) {
    if (this.cooldownUntil > now) return this.cooldownUntil - now;
    if (this.active >= this.maxConcurrency) return 0;
    if (this.startedAt.length < this.rpmLimit) return 0;
    return Math.max(1, RATE_WINDOW_MS - (now - this.startedAt[0]));
  }

  prune(now) {
    const cutoff = now - RATE_WINDOW_MS;
    while (this.startedAt.length && this.startedAt[0] <= cutoff) this.startedAt.shift();
  }
}

function parseJsonFromText(text) {
  const cleaned = String(text || "")
    .replace(/<think>[\s\S]*?<\/think>/gi, "")
    .replace(/```(?:json)?/gi, "")
    .replace(/```/g, "")
    .trim();

  try {
    return JSON.parse(cleaned);
  } catch {
    for (const candidate of jsonObjectCandidates(cleaned).reverse()) {
      try {
        return JSON.parse(candidate);
      } catch {
        // Try the next balanced object.
      }
    }
    return null;
  }
}

function messageContentToText(content) {
  if (typeof content === "string") return content;
  if (Array.isArray(content)) {
    return content.map((part) => {
      if (typeof part === "string") return part;
      return part?.text || part?.content || "";
    }).join("\n");
  }
  return String(content || "");
}

function jsonObjectCandidates(text) {
  const candidates = [];
  let start = -1;
  let depth = 0;
  let inString = false;
  let escaped = false;
  for (let i = 0; i < text.length; i += 1) {
    const char = text[i];
    if (escaped) {
      escaped = false;
      continue;
    }
    if (char === "\\") {
      escaped = inString;
      continue;
    }
    if (char === "\"") {
      inString = !inString;
      continue;
    }
    if (inString) continue;
    if (char === "{") {
      if (depth === 0) start = i;
      depth += 1;
    } else if (char === "}" && depth > 0) {
      depth -= 1;
      if (depth === 0 && start >= 0) candidates.push(text.slice(start, i + 1));
    }
  }
  return candidates;
}

function withProviderFallback(fallback, error, providerMeta = {}) {
  if (fallback && typeof fallback === "object" && !Array.isArray(fallback)) {
    return {
      ...fallback,
      providerFallback: true,
      providerError: error,
      providerMeta
    };
  }
  return fallback;
}

function withProviderMeta(result, providerMeta = {}) {
  if (result && typeof result === "object" && !Array.isArray(result)) {
    return {
      ...result,
      providerMeta
    };
  }
  return result;
}

function delay(ms) {
  if (!ms) return Promise.resolve();
  return new Promise((resolve) => setTimeout(resolve, ms));
}

function retryDelayForAttempt(baseDelayMs, attempt, retryAfter) {
  const retryAfterMs = Number(retryAfter || 0) * 1000;
  if (retryAfterMs > 0) return Math.min(retryAfterMs, 30_000);
  return Math.min(Math.max(0, Number(baseDelayMs || 0)) * Math.max(1, attempt), 15_000);
}

function providerBackoffMs({ retryAfter, code, attempt }) {
  const retryAfterMs = Number(retryAfter || 0) * 1000;
  if (retryAfterMs > 0) return Math.min(retryAfterMs, 120_000);
  if ([1041, 2045, 2056].includes(Number(code))) return Math.min(120_000, 20_000 * Math.max(1, attempt));
  if ([1001, 1002, 1024, 1033].includes(Number(code))) return Math.min(60_000, 5_000 * Math.max(1, attempt));
  return retryDelayForAttempt(DEFAULT_RETRY_DELAY_MS, attempt);
}

function parseProviderError(detail = "") {
  const text = String(detail || "");
  const parsed = parseJsonFromText(text);
  const code = Number(
    parsed?.error_code ||
    parsed?.code ||
    parsed?.base_resp?.status_code ||
    parsed?.baseResp?.statusCode ||
    0
  );
  const message = normalizeProviderMessage(
    parsed?.error_msg ||
    parsed?.message ||
    parsed?.base_resp?.status_msg ||
    parsed?.baseResp?.statusMsg ||
    text
  );
  return { code, message };
}

function normalizeProviderMessage(value) {
  const text = String(value || "").replace(/\s+/g, " ").trim();
  return text.length > 240 ? `${text.slice(0, 240)}...` : text;
}

function isRetryableProviderFailure(status, code) {
  if (status === 429 || status >= 500) return true;
  return [1000, 1001, 1002, 1024, 1033, 1041, 2045].includes(Number(code));
}

function isRetryableNetworkError(error) {
  return /(?:fetch failed|network|econnreset|etimedout|socket|terminated)/i.test(error?.message || String(error || ""));
}

function fallbackSearchQueries(context = {}) {
  return ownerFirstFallbackSearchQueries(context);
}

function fallbackOwnerRabbitHoleStrategy(lead = {}) {
  const name = String(lead.name || "business").trim();
  const city = String(lead.city || "").trim();
  const category = String(lead.category || "").trim();
  const place = [name, city].filter(Boolean).join(" ");
  return {
    pagePaths: ["/about", "/about-us", "/team", "/our-team", "/staff", "/leadership", "/our-story", "/contact", "/contact-us"],
    searchQueries: [
      `"${name}" owner ${city}`.trim(),
      `"${name}" founder ${city}`.trim(),
      `"${name}" CEO ${city}`.trim(),
      `"${place}" LinkedIn owner`,
      `site:linkedin.com/in "${name}" ${city}`.trim(),
      `"${name}" "${category}" owner interview ${city}`.trim()
    ].filter(Boolean),
    profileUrlHints: [],
    ownerNameHints: [],
    strategyReason: "Fallback owner rabbit-hole strategy uses business name, city, owner terms, LinkedIn, and public article searches."
  };
}

function fallbackRunReview(run = {}) {
  const skills = [];
  const avoid = [];
  const counters = run.counters || {};

  if ((counters.qualified || 0) > 0) {
    skills.push({
      title: "Reuse productive search and qualification pattern",
      when: `${run.niche || "requested niche"} lead search starts in a similar USA market`,
      do: "Prioritize search queries and locations that produced qualified leads in this run.",
      avoid: "Restarting from generic queries when productive patterns are already known."
    });
  }

  if ((counters.locationExpansions || 0) > 0) {
    skills.push({
      title: "Expand underperforming territories",
      when: "A planned city or state is under target after exhausting local queries",
      do: "Add nearby USA cities and adjacent markets before ending partial.",
      avoid: "Stopping after the initial location list when the target is still unmet."
    });
  }

  for (const failure of run.failures || []) {
    avoid.push({
      stage: failure.stage || "unknown",
      failure: failure.message || "run failure",
      avoid: "Do not repeat the same settings; change one variable or route around this failure."
    });
  }

  if ((counters.enrichmentFailures || 0) > 0) {
    avoid.push({
      stage: "enrichment",
      failure: "website enrichment failed",
      avoid: "Do not fail the full run on one website; keep qualifying with existing Maps contact data and continue."
    });
  }

  return {
    summary: run.diagnosis || "Run reviewed with local fallback.",
    skills: skills.slice(0, 5),
    avoid: avoid.slice(0, 8)
  };
}

function hasExplicitNonUsCountry(value) {
  return /\b(india|canada|mexico|united kingdom|uk|england|australia|uae|dubai|singapore|germany|france|spain|italy|netherlands|brazil)\b/i.test(value);
}

function hasExplicitDifferentCountry(value, countryScope) {
  if (!isRecognizedCountryScope(countryScope)) return false;
  const text = String(value || "").toLowerCase();
  const target = normalizeCountryScope(countryScope).toLowerCase();
  const aliases = countryAliases(target);
  const countries = [
    "united states",
    "usa",
    "india",
    "canada",
    "mexico",
    "united kingdom",
    "uk",
    "england",
    "australia",
    "uae",
    "dubai",
    "singapore",
    "germany",
    "france",
    "spain",
    "italy",
    "netherlands",
    "brazil",
    "ireland"
  ];
  return countries.some((country) => {
    if (aliases.includes(country)) return false;
    return new RegExp(`\\b${escapeRegExp(country)}\\b`, "i").test(text);
  });
}

function isRecognizedCountryScope(value) {
  const target = normalizeCountryScope(value).toLowerCase();
  return countryAliases(target).some((alias) => {
    return [
      "united states",
      "usa",
      "india",
      "canada",
      "mexico",
      "united kingdom",
      "uk",
      "england",
      "australia",
      "uae",
      "dubai",
      "singapore",
      "germany",
      "france",
      "spain",
      "italy",
      "netherlands",
      "brazil",
      "ireland"
    ].includes(alias);
  });
}

function normalizeCountryScope(value) {
  const raw = String(value || "United States").trim();
  const lower = raw.toLowerCase();
  if (["usa", "us", "u.s.", "u.s.a.", "america"].includes(lower)) return "United States";
  if (lower === "uk") return "United Kingdom";
  if (lower === "uae") return "United Arab Emirates";
  return raw.replace(/\b\w/g, (char) => char.toUpperCase());
}

function countryAliases(target) {
  if (target === "united states") return ["united states", "usa", "us", "u.s.", "u.s.a.", "america"];
  if (target === "united kingdom") return ["united kingdom", "uk", "england"];
  if (target === "united arab emirates") return ["united arab emirates", "uae", "dubai"];
  return [target];
}

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