import { writeFile } from "node:fs/promises";
import path from "node:path";

import { scanOfferContactsByNavigation } from "../playwright/contact-page-navigation.js";
import { isCompletePhoneContactValue } from "../playwright/contact-utils.js";
import { attachVerificationMonitor } from "../playwright/human.js";
import {
  buildSearchBrowserArgs,
  installLowCpuRouteFilters,
} from "../playwright/low-cpu-runtime.js";
import { resolve1688ProfileDir } from "../playwright/profile-dir.js";
import { launch1688PersistentContext } from "../playwright/persistent-context.js";

import type { LlmMatchedSearchCandidate } from "./fixed-search-llm.ts";
import { buildMerchantKey } from "./multi-stage-results.ts";

function normalizeSpace(value: unknown) {
  return String(value ?? "").replace(/\s+/g, " ").trim();
}

function pickReusableContextPage(context: { pages?: () => Array<{ isClosed?: () => boolean }> }) {
  return context.pages?.().find((page) => page && page.isClosed?.() !== true) ?? null;
}

function filterCompletePhoneContacts(contacts: unknown[]) {
  return contacts.filter((contact) => {
    if (!contact || typeof contact !== "object") {
      return false;
    }

    const record = contact as Record<string, unknown>;
    return isCompletePhoneContactValue(record.type, record.value);
  });
}

function isNonBlockingOfferScreenshotIssue(status: string) {
  return Boolean(status && status !== "ok" && status !== "skipped" && status !== "captcha_unresolved");
}

function resolveContactCollectionReason(options: {
  navigationReason: string;
  errorMessage: string;
  offerScreenshotStatus: string;
  contactFetchStatus: string;
  hasPhoneContact: boolean;
}) {
  if (options.hasPhoneContact) {
    return options.navigationReason || options.errorMessage;
  }

  const reason = options.navigationReason || options.errorMessage;
  const reasonLower = reason.toLowerCase();
  const contactFetchStatus = options.contactFetchStatus;

  if (options.offerScreenshotStatus === "captcha_unresolved" || reasonLower.includes("captcha")) {
    return "captcha_unresolved";
  }

  if (contactFetchStatus === "no_contacts" || reason === "contact_page_no_contacts" || reason === "contact_entry_no_contacts") {
    return "contact_page_no_contacts";
  }

  if (isNonBlockingOfferScreenshotIssue(options.offerScreenshotStatus)
    && (contactFetchStatus === "error" || contactFetchStatus === "skipped" || !contactFetchStatus)) {
    return "offer_detail_not_ready_but_contact_failed";
  }

  if (contactFetchStatus === "error"
    || reason === "contact_page_direct_failed"
    || reason === "offer_reload_failed"
    || reason.includes("entry_missing")
    || reason.includes("navigation_failed")) {
    return "contact_navigation_failed";
  }

  return reason || "matched_candidates_exhausted_or_valid_phone_insufficient";
}

function resolveContactShortageReason(attempts: ContactEnrichmentAttempt[], duplicateMerchantCount: number) {
  const rejectedReasons = attempts
    .filter((attempt) => attempt.qualification_status !== "qualified")
    .map((attempt) => normalizeSpace(attempt.contact_collection_reason))
    .filter(Boolean);
  const hasReason = (reason: string) => rejectedReasons.includes(reason);

  if (hasReason("captcha_unresolved")) {
    return "captcha_unresolved";
  }
  if (hasReason("offer_detail_not_ready_but_contact_failed")) {
    return "offer_detail_not_ready_but_contact_failed";
  }
  if (hasReason("contact_navigation_failed")) {
    return "contact_navigation_failed";
  }
  if (hasReason("contact_page_no_contacts")) {
    return "contact_page_no_contacts";
  }
  if (duplicateMerchantCount > 0) {
    return "duplicate_merchants_exhausted";
  }

  return "matched_candidates_exhausted_or_valid_phone_insufficient";
}

export type EnrichedSelectedOffer = {
  index: number;
  input: string;
  "title-text": string;
  url: string;
  score: number;
  is_match: boolean;
  match_bucket: "matched" | "possible" | "not_match";
  reason: string;
  search_round: number;
  round_label: string;
  title: string;
  offer_url: string;
  image_url: string;
  merchant_name: string;
  merchant_url: string;
  wangwang_url: string;
  offer_screenshot_path: string;
  contact_page_url: string;
  detected_contacts: unknown[];
  has_phone_contact: boolean;
  qualification_status: string;
  contact_collection_reason: string;
};

export type ContactEnrichmentAttempt = {
  candidate_index: number;
  phase: "initial_window" | "top_up";
  input: string;
  "title-text": string;
  url: string;
  score: number;
  is_match: boolean;
  match_bucket: "matched" | "possible" | "not_match" | null;
  reason: string;
  search_round: number;
  round_label: string;
  screenshot_path: string;
  contact_page_url: string;
  contactPageUrl: string;
  detected_contacts: unknown[];
  detail_contacts: unknown[];
  contact_page_contacts: unknown[];
  has_phone_contact: boolean;
  offerScreenshotStatus: string;
  contactFetchStatus: string;
  contactFetchMethod: string;
  wangwang_url: string;
  wangwang_original_url: string;
  wangwangFetchStatus: string;
  wangwangFetchMethod: string;
  wangwangFetchReason: string;
  qualification_status: "qualified" | "rejected_no_phone" | "rejected_contact_error" | "rejected_duplicate_merchant";
  contact_collection_reason: string;
  error_message: string;
  inserted_rank: number | null;
};

export type ContactEnrichmentResult = {
  enriched: EnrichedSelectedOffer[];
  attempts: ContactEnrichmentAttempt[];
  attemptedCount: number;
  attemptedCandidateCount: number;
  qualifiedCount: number;
  duplicateMerchantCount: number;
  targetQualifiedCount: number | null;
  initialWindowSize: number;
  shortageReason: string;
  profileDir?: string;
  loginWarmupStatus?: string;
  errorMessage?: string;
};

function isSearchTaskStoppedError(error: unknown) {
  return error instanceof Error && error.name === "SearchTaskStoppedError";
}

async function notifyCandidateAttempt(
  callback: Parameters<typeof enrichSelectedOfferCandidates>[0]["onCandidateAttempt"],
  progress: Parameters<NonNullable<Parameters<typeof enrichSelectedOfferCandidates>[0]["onCandidateAttempt"]>>[0],
) {
  if (!callback) {
    return;
  }

  try {
    await callback(progress);
  } catch (error) {
    if (isSearchTaskStoppedError(error)) {
      throw error;
    }
    console.warn(`[contact-enrichment] 候选进度上报失败，继续补齐联系方式: ${error instanceof Error ? error.message : String(error)}`);
  }
}

function normalizeCandidateSourceProduct(candidate: Partial<LlmMatchedSearchCandidate> & Record<string, unknown>) {
  const source = candidate.source_product && typeof candidate.source_product === "object"
    ? candidate.source_product as Record<string, unknown>
    : {};
  return {
    title: normalizeSpace(source.title ?? candidate["title-text"] ?? candidate.title),
    offer_url: normalizeSpace(source.offer_url ?? candidate.url ?? candidate.offer_url),
    merchant_name: normalizeSpace(source.merchant_name ?? candidate.merchant_name),
    merchant_url: normalizeSpace(source.merchant_url ?? candidate.merchant_url),
    image_url: normalizeSpace(source.image_url ?? candidate.image_url),
    wangwang_url: normalizeSpace(source.wangwang_url ?? candidate.wangwang_url),
    contact_page_url: normalizeSpace(source.contact_page_url ?? candidate.contact_page_url),
    offer_screenshot_path: normalizeSpace(source.offer_screenshot_path ?? candidate.offer_screenshot_path),
  };
}

export function normalizeLlmMatchJsonCandidates(raw: unknown): LlmMatchedSearchCandidate[] {
  const items = Array.isArray(raw)
    ? raw
    : raw && typeof raw === "object" && Array.isArray((raw as Record<string, unknown>).results)
      ? (raw as Record<string, unknown>).results
      : [];

  return items
    .filter((item): item is Record<string, unknown> => Boolean(item) && typeof item === "object")
    .map((item) => {
      const sourceProduct = normalizeCandidateSourceProduct(item);
      const url = normalizeSpace(item.url ?? sourceProduct.offer_url);
      const title = normalizeSpace(item["title-text"] ?? item.title ?? sourceProduct.title);
      return {
        input: normalizeSpace(item.input),
        "title-text": title,
        url,
        score: Number.isFinite(Number(item.score)) ? Number(item.score) : 0,
        base_score: Number.isFinite(Number(item.base_score))
          ? Number(item.base_score)
          : Number.isFinite(Number(item.score))
            ? Number(item.score)
            : 0,
        confidence: Number.isFinite(Number(item.confidence)) ? Number(item.confidence) : null,
        is_match: Boolean(item.is_match),
        reason: normalizeSpace(item.reason),
        search_round: Number.isFinite(Number(item.search_round)) ? Number(item.search_round) : 0,
        round_label: normalizeSpace(item.round_label),
        source_product: {
          ...sourceProduct,
          title: sourceProduct.title || title,
          offer_url: sourceProduct.offer_url || url,
        },
      } satisfies LlmMatchedSearchCandidate;
    })
    .filter((candidate) => candidate.url && candidate["title-text"]);
}

export async function enrichSelectedOfferCandidates(params: {
  candidates: LlmMatchedSearchCandidate[];
  screenshotDir: string;
  headless: boolean;
  profileDir?: string;
  requireLoggedIn?: boolean;
  timeoutMs?: number;
  targetQualifiedCount?: number;
  auditPath?: string;
  onCandidateAttempt?: (progress: {
    candidateIndex: number;
    attemptedCount: number;
    qualifiedCount: number;
    targetQualifiedCount: number | null;
    candidate: LlmMatchedSearchCandidate;
    status: "started" | "qualified" | "rejected_no_phone" | "rejected_contact_error";
    reason: string;
  }) => void | Promise<void>;
}): Promise<ContactEnrichmentResult> {
  const targetQualifiedCount = Number.isInteger(params.targetQualifiedCount)
    && params.targetQualifiedCount != null
    && params.targetQualifiedCount > 0
    ? params.targetQualifiedCount
    : null;

  if (params.candidates.length === 0) {
    const emptyResult: ContactEnrichmentResult = {
      enriched: [],
      attempts: [],
      attemptedCount: 0,
      attemptedCandidateCount: 0,
      qualifiedCount: 0,
      duplicateMerchantCount: 0,
      targetQualifiedCount,
      initialWindowSize: 0,
      shortageReason: targetQualifiedCount ? "matched_candidates_exhausted_or_valid_phone_insufficient" : "",
      profileDir: params.profileDir ? resolve1688ProfileDir({ explicitProfileDir: params.profileDir }) : undefined,
      loginWarmupStatus: "skipped_no_candidates",
    };
    if (params.auditPath) {
      await writeFile(params.auditPath, JSON.stringify(emptyResult, null, 2), "utf8");
    }
    return emptyResult;
  }

  const profileDir = resolve1688ProfileDir({ explicitProfileDir: params.profileDir });
  const context = await launch1688PersistentContext(profileDir, {
    contextOptions: {
      headless: params.headless,
      args: buildSearchBrowserArgs(),
    },
  });
  await installLowCpuRouteFilters(context);
  const verificationMonitor = attachVerificationMonitor(context, { logPrefix: "[fixed-runtime][验证码]" });

  try {
    const loginWarmupStatus = "skipped";

    const enriched: EnrichedSelectedOffer[] = [];
    const attempts: ContactEnrichmentAttempt[] = [];
    const acceptedMerchantKeys = new Set<string>();
    let duplicateMerchantCount = 0;
    let firstCandidatePage = pickReusableContextPage(context);
    const initialWindowSize = targetQualifiedCount == null
      ? params.candidates.length
      : Math.min(targetQualifiedCount, params.candidates.length);

    const processCandidate = async (
      candidate: LlmMatchedSearchCandidate,
      candidateIndex: number,
      phase: ContactEnrichmentAttempt["phase"],
    ) => {
      if (targetQualifiedCount != null && enriched.length >= targetQualifiedCount) {
        return false;
      }

      const candidateMerchantKey = buildMerchantKey({
        merchant_url: candidate.source_product.merchant_url,
        contact_page_url: candidate.source_product.contact_page_url,
        merchant_name: candidate.source_product.merchant_name,
        wangwang_url: candidate.source_product.wangwang_url,
        offer_url: candidate.source_product.offer_url || candidate.url,
        title: candidate.source_product.title || candidate["title-text"],
      });
      if (candidateMerchantKey && acceptedMerchantKeys.has(candidateMerchantKey)) {
        duplicateMerchantCount += 1;
        attempts.push({
          candidate_index: candidateIndex + 1,
          phase,
          input: candidate.input,
          "title-text": candidate["title-text"],
          url: candidate.url,
          score: candidate.score,
          is_match: candidate.is_match,
          match_bucket: candidate.match_bucket,
          reason: candidate.reason,
          search_round: candidate.search_round,
          round_label: candidate.round_label,
          screenshot_path: "",
          contact_page_url: candidate.source_product.contact_page_url,
          contactPageUrl: candidate.source_product.contact_page_url,
          detected_contacts: [],
          detail_contacts: [],
          contact_page_contacts: [],
          has_phone_contact: false,
          offerScreenshotStatus: "skipped",
          contactFetchStatus: "skipped",
          contactFetchMethod: "",
          wangwang_url: candidate.source_product.wangwang_url,
          wangwang_original_url: candidate.source_product.wangwang_url,
          wangwangFetchStatus: "skipped",
          wangwangFetchMethod: "",
          wangwangFetchReason: "duplicate_merchant",
          qualification_status: "rejected_duplicate_merchant",
          contact_collection_reason: "duplicate_merchant",
          error_message: "",
          inserted_rank: null,
        });
        return true;
      }

      const screenshotPath = path.join(params.screenshotDir, `offer-${candidateIndex + 1}.jpg`);
      await notifyCandidateAttempt(params.onCandidateAttempt, {
        candidateIndex,
        attemptedCount: attempts.length + 1,
        qualifiedCount: enriched.length,
        targetQualifiedCount,
        candidate,
        status: "started",
        reason: "",
      });

      let navigationScan: {
        detectedContacts?: unknown;
        preferredTitle?: unknown;
        screenshotPath?: unknown;
        contactPageUrl?: unknown;
        detailContacts?: unknown;
        contactPageContacts?: unknown;
        status?: unknown;
        reason?: unknown;
        offerScreenshotStatus?: unknown;
        contactFetchStatus?: unknown;
        contactFetchMethod?: unknown;
        wangwangUrl?: unknown;
        wangwangFetchStatus?: unknown;
        wangwangFetchMethod?: unknown;
        wangwangFetchReason?: unknown;
      } = {};
      let errorMessage = "";
      try {
        const reusablePage = firstCandidatePage;
        firstCandidatePage = null;
        navigationScan = await scanOfferContactsByNavigation(context, {
          page: reusablePage ?? undefined,
          offerUrl: candidate.url,
          timeoutMs: params.timeoutMs ?? 8000,
          screenshotPath,
          merchantName: candidate.source_product.merchant_name,
          merchantUrl: candidate.source_product.merchant_url,
          contactPageUrl: candidate.source_product.contact_page_url,
          currentTitle: candidate["title-text"],
        });
      } catch (error) {
        errorMessage = error instanceof Error ? error.message : String(error);
      }

      const detectedContacts = Array.isArray(navigationScan.detectedContacts)
        ? navigationScan.detectedContacts
        : [];
      const detailContacts = Array.isArray(navigationScan.detailContacts)
        ? navigationScan.detailContacts
        : [];
      const contactPageContacts = Array.isArray(navigationScan.contactPageContacts)
        ? navigationScan.contactPageContacts
        : [];
      const qualifyingPhoneContacts = filterCompletePhoneContacts(contactPageContacts);
      const hasPhoneContact = qualifyingPhoneContacts.length > 0;
      const offerScreenshotStatus = normalizeSpace(navigationScan.offerScreenshotStatus);
      const offerScreenshotPath = offerScreenshotStatus === "ok"
        ? normalizeSpace(navigationScan.screenshotPath)
        : "";
      const contactFetchStatus = normalizeSpace(navigationScan.contactFetchStatus);
      const contactCollectionReason = resolveContactCollectionReason({
        navigationReason: normalizeSpace(navigationScan.reason),
        errorMessage,
        offerScreenshotStatus,
        contactFetchStatus,
        hasPhoneContact,
      });
      const originalWangwangUrl = candidate.source_product.wangwang_url;
      const resolvedWangwangUrl = normalizeSpace(navigationScan.wangwangUrl) || originalWangwangUrl;
      const baseAttempt = {
        candidate_index: candidateIndex + 1,
        phase,
        input: candidate.input,
        "title-text": candidate["title-text"],
        url: candidate.url,
        score: candidate.score,
        is_match: candidate.is_match,
        match_bucket: candidate.match_bucket,
        reason: candidate.reason,
        search_round: candidate.search_round,
        round_label: candidate.round_label,
        screenshot_path: offerScreenshotPath,
        contact_page_url: normalizeSpace(navigationScan.contactPageUrl) || candidate.source_product.contact_page_url,
        contactPageUrl: normalizeSpace(navigationScan.contactPageUrl) || candidate.source_product.contact_page_url,
        detected_contacts: detectedContacts,
        detail_contacts: detailContacts,
        contact_page_contacts: contactPageContacts,
        has_phone_contact: hasPhoneContact,
        offerScreenshotStatus,
        contactFetchStatus,
        contactFetchMethod: normalizeSpace(navigationScan.contactFetchMethod),
        wangwang_url: resolvedWangwangUrl,
        wangwang_original_url: originalWangwangUrl,
        wangwangFetchStatus: normalizeSpace(navigationScan.wangwangFetchStatus),
        wangwangFetchMethod: normalizeSpace(navigationScan.wangwangFetchMethod),
        wangwangFetchReason: normalizeSpace(navigationScan.wangwangFetchReason),
        contact_collection_reason: contactCollectionReason,
        error_message: errorMessage,
      };

      if (!hasPhoneContact) {
        const scanStatus = normalizeSpace(navigationScan.status);
        const status = errorMessage || (scanStatus && scanStatus !== "ok" && scanStatus !== "skipped")
          ? "rejected_contact_error"
          : "rejected_no_phone";
        attempts.push({
          ...baseAttempt,
          qualification_status: status,
          inserted_rank: null,
        });
        await notifyCandidateAttempt(params.onCandidateAttempt, {
          candidateIndex,
          attemptedCount: attempts.length,
          qualifiedCount: enriched.length,
          targetQualifiedCount,
          candidate,
          status,
          reason: contactCollectionReason,
        });
        return true;
      }

      const enrichedOffer = {
        index: enriched.length + 1,
        input: candidate.input,
        "title-text": candidate["title-text"],
        url: candidate.url,
        score: candidate.score,
        is_match: candidate.is_match,
        match_bucket: candidate.match_bucket ?? "not_match",
        reason: candidate.reason,
        search_round: candidate.search_round,
        round_label: candidate.round_label,
        title: candidate.source_product.title || candidate["title-text"],
        offer_url: candidate.source_product.offer_url || candidate.url,
        image_url: candidate.source_product.image_url,
        merchant_name: candidate.source_product.merchant_name,
        merchant_url: candidate.source_product.merchant_url,
        wangwang_url: resolvedWangwangUrl,
        offer_screenshot_path: offerScreenshotPath || candidate.source_product.offer_screenshot_path,
        contact_page_url: normalizeSpace(navigationScan.contactPageUrl) || candidate.source_product.contact_page_url,
        detected_contacts: qualifyingPhoneContacts,
        has_phone_contact: true,
        qualification_status: "qualified",
        contact_collection_reason: contactCollectionReason,
      } satisfies EnrichedSelectedOffer;
      enriched.push(enrichedOffer);
      if (candidateMerchantKey) {
        acceptedMerchantKeys.add(candidateMerchantKey);
      }
      attempts.push({
        ...baseAttempt,
        qualification_status: "qualified",
        inserted_rank: enrichedOffer.index,
      });
      await notifyCandidateAttempt(params.onCandidateAttempt, {
        candidateIndex,
        attemptedCount: attempts.length,
        qualifiedCount: enriched.length,
        targetQualifiedCount,
        candidate,
        status: "qualified",
        reason: contactCollectionReason,
      });
      return true;
    };

    for (let candidateIndex = 0; candidateIndex < initialWindowSize; candidateIndex += 1) {
      await processCandidate(params.candidates[candidateIndex]!, candidateIndex, "initial_window");
    }

    for (let candidateIndex = initialWindowSize; candidateIndex < params.candidates.length; candidateIndex += 1) {
      if (targetQualifiedCount != null && enriched.length >= targetQualifiedCount) {
        break;
      }
      await processCandidate(params.candidates[candidateIndex]!, candidateIndex, "top_up");
    }

    const result: ContactEnrichmentResult = {
      enriched,
      attempts,
      attemptedCount: attempts.length,
      attemptedCandidateCount: attempts.length,
      qualifiedCount: enriched.length,
      duplicateMerchantCount,
      targetQualifiedCount,
      initialWindowSize,
      shortageReason: targetQualifiedCount != null && enriched.length < targetQualifiedCount
        ? resolveContactShortageReason(attempts, duplicateMerchantCount)
        : "",
      profileDir,
      loginWarmupStatus,
    };

    if (params.auditPath) {
      await writeFile(params.auditPath, JSON.stringify(result, null, 2), "utf8");
    }

    return result;
  } finally {
    verificationMonitor.dispose();
    await context.close().catch(() => {});
  }
}
