export type FixedSearchRoundStatus = {
  round: number;
  label: string;
  query: string;
  status: "success" | "failed" | "duplicate_skip";
  failure_reason: string;
};

export type SearchCandidateLike = {
  title?: unknown;
  offer_url?: unknown;
  offerUrl?: unknown;
  merchant_name?: unknown;
  merchantName?: unknown;
  merchant_url?: unknown;
  merchantUrl?: unknown;
  image_url?: unknown;
  imageUrl?: unknown;
  wangwang_url?: unknown;
  contact?: { url?: unknown } | null;
  contact_page_url?: unknown;
  contactPageUrl?: unknown;
  offer_screenshot_path?: unknown;
  offerScreenshotPath?: unknown;
};

export type ScoredSearchCandidate = {
  input: string;
  "title-text": string;
  url: string;
  score: number;
  search_round: number;
  round_label: string;
  source_product: {
    title: string;
    offer_url: string;
    merchant_name: string;
    merchant_url: string;
    image_url: string;
    wangwang_url: string;
    contact_page_url: string;
    offer_screenshot_path: string;
  };
};

export type SemanticCandidateHitRateInput = {
  is_match?: boolean | null;
  judge_status?: string | null;
};

const FIXED_RUNTIME_FIELD_WEIGHTS = {
  brand: 1,
  primary_search_term: 0.8,
  standardized_name: 0.6,
  standardized_spec: 0.4,
  category: 0.2,
} as const;

const ROUND_LABEL_WEIGHT_FACTORS: Record<string, number[]> = {
  主搜索词: [FIXED_RUNTIME_FIELD_WEIGHTS.primary_search_term],
  标准化名称: [FIXED_RUNTIME_FIELD_WEIGHTS.standardized_name],
  "标准化名称+品牌": [FIXED_RUNTIME_FIELD_WEIGHTS.standardized_name, FIXED_RUNTIME_FIELD_WEIGHTS.brand],
  "品牌+标准化名称": [FIXED_RUNTIME_FIELD_WEIGHTS.brand, FIXED_RUNTIME_FIELD_WEIGHTS.standardized_name],
  "标准化名称+标准化规格": [
    FIXED_RUNTIME_FIELD_WEIGHTS.standardized_name,
    FIXED_RUNTIME_FIELD_WEIGHTS.standardized_spec,
  ],
  "标准化规格+标准化名称": [
    FIXED_RUNTIME_FIELD_WEIGHTS.standardized_spec,
    FIXED_RUNTIME_FIELD_WEIGHTS.standardized_name,
  ],
  "标准化名称+分类(材质/用途)": [FIXED_RUNTIME_FIELD_WEIGHTS.standardized_name, FIXED_RUNTIME_FIELD_WEIGHTS.category],
};

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

function normalizeCompactText(value: unknown) {
  return normalizeSpace(value)
    .normalize("NFKC")
    .replace(/[\u00a0\s]+/g, "")
    .trim();
}

function isGeneratedFallbackTitle(value: unknown) {
  const normalized = normalizeSpace(value);
  return /^1688结果-\d+$/i.test(normalized);
}

export function normalizeOfferDetailUrl(value: unknown) {
  const raw = normalizeSpace(value);
  if (!raw) {
    return "";
  }

  try {
    const url = new URL(raw.startsWith("//") ? `https:${raw}` : raw);
    const offerIdMatch = url.pathname.match(/\/offer\/(\d+)\.html/i);
    if (offerIdMatch?.[1]) {
      return `${url.protocol}//${url.host}/offer/${offerIdMatch[1]}.html`;
    }

    const offerId = url.searchParams.get("offerId") ?? url.searchParams.get("id") ?? "";
    if (offerId) {
      return `https://detail.1688.com/offer/${offerId}.html`;
    }

    url.search = "";
    url.hash = "";
    return url.toString();
  } catch {
    return raw.split(/[?#]/, 1)[0] ?? raw;
  }
}

export function scoreTitleAgainstQuery(query: unknown, title: unknown) {
  if (isGeneratedFallbackTitle(title)) {
    return 0;
  }

  const normalizedQuery = normalizeCompactText(query);
  const normalizedTitle = normalizeCompactText(title);

  if (!normalizedQuery || !normalizedTitle) {
    return 0;
  }

  let hitCount = 0;
  for (const character of normalizedQuery) {
    if (normalizedTitle.includes(character)) {
      hitCount += 1;
    }
  }

  return Math.floor((hitCount / normalizedQuery.length) * 100);
}

export function resolveRoundWeight(roundLabel: unknown) {
  const normalizedRoundLabel = normalizeSpace(roundLabel);
  const factors = ROUND_LABEL_WEIGHT_FACTORS[normalizedRoundLabel];
  if (!factors || factors.length === 0) {
    return 1;
  }

  return factors.reduce((accumulator, factor) => accumulator * factor, 1);
}

export function applyRoundWeightToScore(score: unknown, roundLabel: unknown) {
  const numericScore = Number(score);
  if (!Number.isFinite(numericScore) || numericScore <= 0) {
    return 0;
  }

  const weightedScore = numericScore * resolveRoundWeight(roundLabel);
  return Number(weightedScore.toFixed(2));
}

function normalizeCandidateProduct(product: SearchCandidateLike) {
  return {
    title: normalizeSpace(product.title),
    offer_url: normalizeOfferDetailUrl(product.offer_url ?? product.offerUrl),
    merchant_name: normalizeSpace(product.merchant_name ?? product.merchantName),
    merchant_url: normalizeSpace(product.merchant_url ?? product.merchantUrl),
    image_url: normalizeSpace(product.image_url ?? product.imageUrl),
    wangwang_url: normalizeSpace(product.wangwang_url ?? product.contact?.url),
    contact_page_url: normalizeSpace(product.contact_page_url ?? product.contactPageUrl),
    offer_screenshot_path: normalizeSpace(product.offer_screenshot_path ?? product.offerScreenshotPath),
  };
}

export function buildScoredRoundCandidates(params: {
  round: FixedSearchRoundStatus;
  products: SearchCandidateLike[];
}) {
  const seenUrls = new Set<string>();
  const scored: ScoredSearchCandidate[] = [];

  for (const product of Array.isArray(params.products) ? params.products : []) {
    const normalizedProduct = normalizeCandidateProduct(product);
    if (!normalizedProduct.offer_url || !normalizedProduct.title) {
      continue;
    }

    if (seenUrls.has(normalizedProduct.offer_url)) {
      continue;
    }
    seenUrls.add(normalizedProduct.offer_url);

    scored.push({
      input: params.round.query,
      "title-text": normalizedProduct.title,
      url: normalizedProduct.offer_url,
      score: applyRoundWeightToScore(
        scoreTitleAgainstQuery(params.round.query, normalizedProduct.title),
        params.round.label,
      ),
      search_round: params.round.round,
      round_label: params.round.label,
      source_product: normalizedProduct,
    });
  }

  return scored;
}

export function mergeScoredCandidates(candidates: ScoredSearchCandidate[]) {
  const merged = new Map<string, ScoredSearchCandidate>();

  for (const candidate of candidates) {
    const existing = merged.get(candidate.url);
    if (!existing) {
      merged.set(candidate.url, candidate);
      continue;
    }

    const shouldReplace = candidate.score > existing.score
      || (candidate.score === existing.score && candidate.search_round < existing.search_round);

    if (shouldReplace) {
      merged.set(candidate.url, candidate);
    }
  }

  return [...merged.values()].sort((left, right) => (
    right.score - left.score
    || left.search_round - right.search_round
    || left.url.localeCompare(right.url, "zh-CN")
  ));
}

export function computeSemanticCandidateHitRate(candidates: SemanticCandidateHitRateInput[]) {
  const judgedCandidates = candidates.filter((candidate) => candidate.judge_status === "judged");
  const matchedCandidates = judgedCandidates.filter((candidate) => candidate.is_match === true);
  const errorCandidates = candidates.filter((candidate) => candidate.judge_status === "llm_error");
  const unjudgedCandidates = candidates.filter((candidate) => candidate.judge_status !== "judged");
  return {
    hit_rate: judgedCandidates.length > 0 ? Number((matchedCandidates.length / judgedCandidates.length).toFixed(4)) : null,
    matched_count: matchedCandidates.length,
    judged_count: judgedCandidates.length,
    unjudged_count: unjudgedCandidates.length,
    error_count: errorCandidates.length,
  };
}
