import type {
  Brand,
  BrandProduct,
  CostPreview,
  DemandMap,
  DemandMapPhrase,
  Opportunity,
  OpportunityRemovalReason,
  OpportunityReviewState,
  RadarToday,
  RadarSweep,
  RadarWatch,
  ServiceProfile,
  WatchTermSuggestion,
} from "../../domain/types";
import type { RadarRepo } from "../repos";
import { ApiError } from "../live/http";
import { productRadarSeed } from "../fixtures/radarProduct";
import { expandIncludeTerms, searchableTermVariants } from "../../lib/threadsTerm";

type MockRadarSeed = {
  brands?: Brand[];
  products?: BrandProduct[];
  watches?: RadarWatch[];
  opportunities?: Opportunity[];
  demandMaps?: DemandMap[];
  profile?: Partial<ServiceProfile>;
};

const nanoNow = () => Date.now() * 1_000_000;
const copy = <T>(value: T): T => structuredClone(value);
const notReady = (name: string): never => {
  throw new ApiError(`${name} is not ready`, 501010, 501);
};

function error(message: string, code = 400001): never {
  throw new ApiError(message, code, code >= 500000 ? 501 : 400);
}

const DAY_NS = 86_400_000_000_000;

function phrase(text: string, kind: string, origin: DemandMapPhrase["origin"] = "product", basisText?: string): DemandMapPhrase {
  return { text, kind, origin, basis_kind: origin === "product" ? kind : "custom", basis_text: basisText, enabled: true };
}

function searchablePhrases(items: string[], kind: string, basisLabel: string, include: boolean): DemandMapPhrase[] {
  const out: DemandMapPhrase[] = [];
  for (const item of items) {
    const raw = item.trim();
    if (!raw) continue;
    if (!include) {
      out.push(phrase(raw, kind, "product", basisLabel));
      continue;
    }
    for (const text of searchableTermVariants(raw).slice(0, 3)) {
      out.push(phrase(text, kind, "product", raw));
    }
  }
  return out;
}

function baselineDemandMap(product: BrandProduct): DemandMap {
  const pain = searchablePhrases(product.pain_points, "pain", "產品痛點", true);
  const scenario = searchablePhrases(product.product_context.trim() ? [product.product_context.trim()] : [], "scenario", "產品情境", true);
  const outcome = searchablePhrases(product.match_tags, "outcome", "產品標籤", true);
  const solution = searchablePhrases(product.provider_capability_terms, "solution", "產品能力", true);
  const exclusion = searchablePhrases(product.provider_exclude_terms, "exclusion", "排除詞", false);
  const ready = pain.length > 0 && scenario.length > 0 && solution.length > 0;
  return {
    product_id: product.id,
    demand_input_version: `product-${product.updated_at}`,
    map_version: 1,
    state: ready ? "ready" : "incomplete",
    pain_phrases: pain,
    scenario_phrases: scenario,
    desired_outcomes: outcome,
    solution_signals: solution,
    exclusion_signals: exclusion,
    source_basis: [product.label, product.product_context].filter(Boolean),
    custom_phrases: [],
    updated_at: product.updated_at || nanoNow(),
  };
}

function reviewState(row: Opportunity): OpportunityReviewState {
  if (row.review_state) return row.review_state;
  // Existing rows predate the inbox. Preserve their user-visible meaning while
  // making the new state explicit at the repository boundary.
  if (row.status === "accepted") return "completed";
  if (row.status === "dismissed") return "removed";
  return "pending";
}

function withReviewDefaults(row: Opportunity): Opportunity {
  return { ...copy(row), review_state: reviewState(row) };
}

function removalReason(value: string | undefined): OpportunityRemovalReason {
  const allowed: OpportunityRemovalReason[] = [
    "pain_mismatch", "provider_or_ad", "stale", "already_solved", "duplicate", "other", "legacy_unknown",
  ];
  return value && allowed.includes(value as OpportunityRemovalReason)
    ? value as OpportunityRemovalReason
    : "legacy_unknown";
}

/** In-memory product-aware RadarRepo used by Phase B pages and contract tests. */
export function createMockRadarRepo(seed: MockRadarSeed = {}): RadarRepo {
  const effectiveSeed = Object.keys(seed).length ? seed : productRadarSeed;
  const brands = new Map((effectiveSeed.brands ?? []).map((b) => [b.id, copy(b)]));
  const products = new Map((effectiveSeed.products ?? []).map((p) => [p.id, copy(p)]));
  const watches = new Map((seed.watches ?? []).map((w) => [w.id, copy(w)]));
  const sweeps = new Map<string, RadarSweep>();
  const opportunities = new Map((effectiveSeed.opportunities ?? []).map((o) => [o.id, withReviewDefaults(o)]));
  const demandMaps = new Map((seed.demandMaps ?? []).map((map) => [map.product_id, copy(map)]));
  const profile: ServiceProfile = {
    exists: false,
    services: [],
    cases: [],
    forbidden: [],
    faq: [],
    service_areas: [],
    remote_ok: false,
    ...copy(effectiveSeed.profile ?? {}),
  };

  function productFor(brandId: string, productId: string): BrandProduct {
    const brand = brands.get(brandId);
    const product = products.get(productId);
    if (!brand || !product || product.brand_id !== brandId) error("brand or product not found", 404001);
    return product;
  }

  function watch(id: string): RadarWatch {
    const found = watches.get(id);
    if (!found) error("watch not found", 404001);
    return found;
  }

  function today(filter: { brand_id?: string; product_id?: string; fit_band?: string } = {}): RadarToday {
    const eligible = [...opportunities.values()]
      .filter((o) => reviewState(o) === "pending")
      .filter((o) => o.status !== "rejected" && o.status !== "dismissed")
      .filter((o) => {
        const matches = o.product_matches ?? [];
        if (filter.brand_id && !matches.some((m) => m.brand_id === filter.brand_id)) return false;
        if (filter.product_id && !matches.some((m) => m.product_id === filter.product_id)) return false;
        if (filter.fit_band && !matches.some((m) => m.product_fit_band === filter.fit_band && m.eligible)) return false;
        return !o.product_matches?.length || matches.some((m) => m.eligible && !m.excluded);
      })
      .sort((a, b) => b.intent_score - a.intent_score || b.created_at - a.created_at);
    const high = eligible.filter((o) => o.intent_band === "high");
    const mid = eligible.filter((o) => o.intent_band === "mid");
    const low = eligible.filter((o) => o.intent_band === "low");
    return {
      stats: { total: eligible.length, high: high.length, mid: mid.length, low: low.length },
      high: copy(high),
      mid: copy(mid),
      low: copy(low),
      truncated_count: 0,
      empty_reason: eligible.length ? undefined : "no_eligible_product_match",
      empty_hint: eligible.length ? undefined : "前往全部結果檢查 weak／stale／excluded 資料",
    };
  }

  return {
    async getServiceProfile() {
      return copy(profile);
    },
    async saveServiceProfile(patch) {
      Object.assign(profile, copy(patch), { exists: true, updated_at: nanoNow() });
      return copy(profile);
    },
    async listWatches(page = 1, pageSize = 20, status, contextMode, brandId, productId) {
      const all = [...watches.values()].filter((w) =>
        (!status || w.status === status) &&
        (!contextMode || (w.context_mode ?? "generic") === contextMode) &&
        (!brandId || w.brand_id === brandId) &&
        (!productId || w.product_id === productId),
      );
      return {
        list: copy(all.slice((page - 1) * pageSize, page * pageSize)),
        total: all.length,
        active_count: [...watches.values()].filter((w) => w.status === "active").length,
        max_active: 10,
        profile_exists: profile.exists,
      };
    },
    async createWatch(input) {
      const productMode = Boolean(input.brand_id || input.product_id);
      if (productMode && (!input.brand_id || !input.product_id)) error("brand_id and product_id must be provided together");
      if (productMode) productFor(input.brand_id!, input.product_id!);
      const terms = expandIncludeTerms(input.terms);
      if (!terms.length) error("terms required");
      const id = `watch-${watches.size + 1}`;
      const now = nanoNow();
      const row: RadarWatch = {
        id,
        terms,
        exclude_terms: copy(input.exclude_terms ?? []),
        regions: copy(input.regions ?? []),
        status: input.enabled === false ? "paused" : "active",
        context_mode: productMode ? "product" : "generic",
        brand_id: input.brand_id,
        product_id: input.product_id,
        brand_name_snapshot: input.brand_id ? brands.get(input.brand_id)?.display_name : undefined,
        product_label_snapshot: input.product_id ? products.get(input.product_id)?.label : undefined,
        context_bound_at: productMode ? now : undefined,
        created_at: now,
        updated_at: now,
      };
      watches.set(id, row);
      return copy(row);
    },
    async assignWatchProduct(id, brandId, productId) {
      const row = watch(id);
      if ((row.context_mode ?? "generic") !== "generic" || row.brand_id || row.product_id) error("generic watch can only be assigned once", 409001);
      const product = productFor(brandId, productId);
      const now = nanoNow();
      Object.assign(row, {
        context_mode: "product",
        brand_id: brandId,
        product_id: productId,
        brand_name_snapshot: brands.get(brandId)?.display_name,
        product_label_snapshot: product.label,
        context_bound_at: now,
        updated_at: now,
      });
      return copy(row);
    },
    async updateWatch(id, patch) {
      const row = watch(id);
      const next = { ...copy(patch) };
      if (patch.terms) {
        next.terms = expandIncludeTerms(patch.terms);
        if (!next.terms.length) error("terms required");
      }
      Object.assign(row, next, { updated_at: nanoNow() });
      return copy(row);
    },
    async pauseWatch(id) {
      const row = watch(id);
      row.status = "paused";
      row.pause_reason = "user";
      row.updated_at = nanoNow();
      return copy(row);
    },
    async resumeWatch(id) {
      const row = watch(id);
      if (row.pause_reason === "product_unavailable" || row.pause_reason === "brand_unavailable") error("unavailable product watch must be archived and recreated", 409001);
      row.status = "active";
      row.pause_reason = undefined;
      row.updated_at = nanoNow();
      return copy(row);
    },
    async archiveWatch(id) {
      const row = watch(id);
      row.status = "archived";
      row.updated_at = nanoNow();
    },
    async deleteArchivedWatch(id) {
      const row = watch(id);
      if (row.status !== "archived") error("only archived watch can be permanently deleted", 400100);
      watches.delete(id);
    },
    async suggestWatchTerms(input) {
      const parsed = typeof input === "number" ? { limit: input } : input ?? {};
      const limit = parsed.limit ?? 8;
      const product = parsed.product_id ? products.get(parsed.product_id) : undefined;
      if (!product) {
        return [
          { term: "求推薦", reason: "台灣 Threads 上找服務最常這樣問", usage: "include" as const },
          { term: "有人知道", reason: "口語求助句，能找到正在發問的人", usage: "include" as const },
          { term: "求建議", reason: "正在比較或猶豫的人常這樣寫", usage: "include" as const },
          { term: "哪裡找", reason: "明確在找店家或服務的人", usage: "include" as const },
          { term: "徵才", reason: "招募文不是客人", usage: "exclude" as const },
        ].slice(0, limit);
      }
      const fields: Array<{ kind: WatchTermSuggestion["basis_kind"]; label: string; terms: string[]; usage: WatchTermSuggestion["usage"] }> = [
        { kind: "pain", label: "痛點", terms: product.pain_points, usage: "include" },
        { kind: "tag", label: "標籤", terms: product.match_tags, usage: "include" },
        { kind: "capability", label: "能力", terms: product.provider_capability_terms, usage: "include" },
        { kind: "audience", label: "受眾", terms: [], usage: "include" },
        { kind: "context", label: "情境", terms: product.product_context.trim() ? [product.product_context.trim()] : [], usage: "include" },
        { kind: "exclude", label: "排除詞", terms: product.provider_exclude_terms, usage: "exclude" },
      ];
      const out: WatchTermSuggestion[] = [];
      const seen = new Set<string>();
      for (const field of fields) {
        for (const basis of field.terms) {
          const raw = basis.trim();
          if (!raw) continue;
          const variants = field.usage === "include" ? searchableTermVariants(raw) : [raw];
          for (const term of variants) {
            const key = `${field.usage}:${term.toLowerCase()}`;
            if (seen.has(key)) continue;
            seen.add(key);
            out.push({
              term,
              reason: field.usage === "exclude"
                ? `產品設定將「${raw}」列為排除詞，避免混入非目標貼文`
                : `依產品設定的${field.label}「${raw}」，可找相關需求貼文`,
              usage: field.usage,
              basis_kind: field.kind,
              basis_text: raw,
            });
            if (out.length >= limit) return out;
          }
        }
      }
      return out;
    },
    async triggerWatchSweep(id) {
      const row = watch(id);
      const started = nanoNow();
      const ended = nanoNow();
      const candidates = [...opportunities.values()].filter((opportunity) => !opportunity.watch_id || opportunity.watch_id === id);
      const sweep: RadarSweep = {
        id: `sweep-${sweeps.size + 1}`,
        watch_id: id,
        job_id: `job-${sweeps.size + 1}`,
        path: "api",
        hit_count: candidates.length,
        judged_count: candidates.length,
        created_count: candidates.filter((opportunity) => reviewState(opportunity) === "pending").length,
        truncated_count: 0,
        match_evaluated_count: candidates.length,
        match_merged_count: candidates.filter((opportunity) => (opportunity.product_matches?.length ?? 0) > 1).length,
        fit_rejected_count: candidates.filter((opportunity) => (opportunity.product_matches ?? []).every((match) => !match.eligible)).length,
        demand_input_version: row.product_id ? `product-${products.get(row.product_id)?.updated_at ?? 0}` : undefined,
        demand_map_version: row.product_id ? demandMaps.get(row.product_id)?.map_version : undefined,
        deduped_count: 0,
        prefilter_pass_count: candidates.length,
        prefilter_review_count: candidates.length,
        prefilter_rejected_count: 0,
        cached_judgment_count: 0,
        tombstone_matched_count: candidates.filter((opportunity) => reviewState(opportunity) === "removed").length,
        budget_deferred_count: 0,
        sweep_status: "complete",
        credit_search: 1,
        credit_demand_map: 0,
        credit_judge: candidates.length ? 1 : 0,
        credit_reply: 0,
        credits_used: candidates.length ? 2 : 1,
        started_at: started,
        ended_at: ended,
      };
      sweeps.set(sweep.id, sweep);
      row.last_swept_at = ended;
      row.updated_at = ended;
      for (const opportunity of candidates) {
        opportunity.last_matched_at = ended;
        const state = reviewState(opportunity);
        if (state !== "removed" && state !== "completed") {
          opportunity.review_state = "pending";
        }
      }
      return { job_id: sweep.job_id!, sweep_id: sweep.id };
    },
    async getToday(filter) {
      return today(filter);
    },
    async listOpportunities(filter = {}) {
      const list = [...opportunities.values()]
        .filter((o) => !filter.band || o.intent_band === filter.band)
        .filter((o) => !filter.status || o.status === filter.status)
        .filter((o) => !filter.review_state || reviewState(o) === filter.review_state)
        .filter((o) => !filter.brand_id || (o.product_matches ?? []).some((m) => m.brand_id === filter.brand_id))
        .filter((o) => !filter.product_id || (o.product_matches ?? []).some((m) => m.product_id === filter.product_id))
        .filter((o) => !filter.fit_band || (o.product_matches ?? []).some((m) => m.product_fit_band === filter.fit_band))
        .filter((o) => {
          if (filter.time_scope === "all" || !filter.time_scope) return true;
          const now = Date.now() * 1_000_000;
          const start = filter.time_scope === "today"
            ? new Date(new Date().setHours(0, 0, 0, 0)).getTime() * 1_000_000
            : now - 7 * DAY_NS;
          const posted = o.posted_at ?? 0;
          const discovered = o.last_matched_at || o.created_at || 0;
          return posted >= start || discovered >= start;
        })
        .sort((a, b) => {
          switch (filter.sort) {
            case "newest": return b.posted_at - a.posted_at || b.id.localeCompare(a.id);
            case "oldest": return a.posted_at - b.posted_at || a.id.localeCompare(b.id);
            case "product_fit": return (b.primary_product_fit_score ?? 0) - (a.primary_product_fit_score ?? 0) || b.intent_score - a.intent_score;
            case "demand_intent": return (b.priority_factors?.demand_intent ?? b.intent_score) - (a.priority_factors?.demand_intent ?? a.intent_score) || b.posted_at - a.posted_at;
            default: return (b.priority_score ?? b.intent_score) - (a.priority_score ?? a.intent_score) || b.intent_score - a.intent_score || b.posted_at - a.posted_at || a.id.localeCompare(b.id);
          }
        });
      const page = filter.page ?? 1;
      const size = filter.pageSize ?? 20;
      return { list: copy(list.slice((page - 1) * size, page * size).map(withReviewDefaults)), total: list.length };
    },
    async getOpportunity(id) {
      return withReviewDefaults(opportunities.get(id) ?? error("opportunity not found", 404001));
    },
    async acceptOpportunity(id) {
      const row = opportunities.get(id);
      if (!row) error("opportunity not found", 404001);
      row.status = "accepted";
      row.review_state = "completed";
      row.removal_reason = undefined;
      row.removal_note = undefined;
      row.removed_at = undefined;
      return { opportunity_id: id, status: row.status };
    },
    async dismissOpportunity(id, reason) {
      const row = opportunities.get(id);
      if (!row) error("opportunity not found", 404001);
      row.status = "dismissed";
      row.previous_review_state = reviewState(row);
      row.review_state = "removed";
      row.removal_reason = removalReason(reason);
      row.removal_note = reason && reason.trim() !== row.removal_reason ? reason.trim() : undefined;
      row.removed_at = nanoNow();
      return withReviewDefaults(row);
    },
    async updateOpportunityReviewState(id, patch) {
      const row = opportunities.get(id);
      if (!row) error("opportunity not found", 404001);
      const current = reviewState(row);
      if (patch.state === "removed") {
        if (!patch.removal_reason) error("removal_reason required");
        if (patch.removal_reason === "other" && !patch.removal_note?.trim()) error("removal_note required");
        if (patch.removal_reason === "duplicate") {
          if (!patch.duplicate_of_opportunity_id || patch.duplicate_of_opportunity_id === id) error("duplicate target required");
          const target = opportunities.get(patch.duplicate_of_opportunity_id);
          if (!target || target.author_handle !== row.author_handle) error("duplicate target must belong to the same author", 409001);
        }
        if (current === "removed" && row.removal_reason === patch.removal_reason && row.removal_note === patch.removal_note?.trim()) {
          return withReviewDefaults(row);
        }
        row.previous_review_state = current;
        row.review_state = "removed";
        row.removal_reason = patch.removal_reason;
        row.removal_note = patch.removal_note?.trim() || undefined;
        row.removed_at = nanoNow();
      } else if (patch.state === "pending" || patch.state === "completed") {
        if (current === patch.state && !row.removal_reason) return withReviewDefaults(row);
        row.review_state = patch.state;
        if (current === "removed") row.previous_review_state = current;
        row.removal_reason = undefined;
        row.removal_note = undefined;
        row.removed_at = undefined;
        if (patch.state === "completed") row.status = "accepted";
      } else {
        error("invalid review state");
      }
      return withReviewDefaults(row);
    },
    async overrideOpportunity(id, patch) {
      const row = opportunities.get(id);
      if (!row) error("opportunity not found", 404001);
      if (patch.band) row.intent_band = patch.band as Opportunity["intent_band"];
      return copy(row);
    },
    async setPrimaryProduct(id, productId, reason) {
      if (!reason.trim()) error("reason required");
      const row = opportunities.get(id);
      if (!row) error("opportunity not found", 404001);
      const match = row.product_matches?.find((m) => m.product_id === productId);
      if (!match) error("primary product must be an eligible match");
      row.primary_brand_id = match.brand_id;
      row.primary_product_id = match.product_id;
      row.primary_brand_name = match.brand_name_snapshot;
      row.primary_product_label = match.product_label_snapshot;
      row.primary_product_fit_score = match.product_fit_score;
      row.primary_product_fit_band = match.product_fit_band;
      row.primary_product_overridden = true;
      return copy(row);
    },
    async listReplies(): Promise<never> { return notReady("radar.replies"); },
    async createReply(): Promise<never> { return notReady("radar.replies"); },
    async markReplyUsed(): Promise<never> { return notReady("radar.replies"); },
    async listSweeps(page = 1, pageSize = 20, watchId) {
      const all = [...sweeps.values()].filter((sweep) => !watchId || sweep.watch_id === watchId).reverse();
      return { list: copy(all.slice((page - 1) * pageSize, page * pageSize)), total: all.length };
    },
    async importOpportunities(): Promise<never> { return notReady("radar.import"); },
    async exploreOpportunities(): Promise<never> { return notReady("radar.explore"); },
    async getDemandMap(productId) {
      const product = products.get(productId);
      if (!product) error("product not found", 404001);
      const existing = demandMaps.get(productId);
      if (existing) return copy(existing);
      const created = baselineDemandMap(product);
      demandMaps.set(productId, created);
      return copy(created);
    },
    async updateDemandMap(productId, patch) {
      const current = demandMaps.get(productId) ?? await this.getDemandMap(productId);
      if (patch.expected_map_version !== current.map_version) error("demand map version conflict", 409002);
      const next: DemandMap = {
        product_id: productId,
        demand_input_version: patch.demand_input_version,
        map_version: current.map_version + 1,
        state: patch.state,
        pain_phrases: copy(patch.pain_phrases),
        scenario_phrases: copy(patch.scenario_phrases),
        desired_outcomes: copy(patch.desired_outcomes),
        solution_signals: copy(patch.solution_signals),
        exclusion_signals: copy(patch.exclusion_signals),
        source_basis: copy(patch.source_basis),
        custom_phrases: copy(patch.custom_phrases),
        ai_enriched_at: patch.ai_enriched_at,
        updated_at: nanoNow(),
      };
      demandMaps.set(productId, next);
      return copy(next);
    },
    async enrichDemandMap(): Promise<never> { return notReady("radar.demand_map.enrich"); },
    async getCostPreview(input) {
      const candidateLimit = Math.max(1, input.candidate_limit ?? (input.action === "explore" ? 12 : 20));
      const searchCalls = Math.min(6, Math.max(1, Math.ceil(candidateLimit / 4)));
      const fixedCredits = input.action === "demand_map_enrich" ? 1 : 0;
      const preview: CostPreview = {
        preview_id: `preview-${input.action}-${Date.now()}`,
        action: input.action,
        key_mode: "platform",
        fixed_credits: fixedCredits,
        min_credits: fixedCredits,
        max_credits: fixedCredits + searchCalls + Math.ceil(candidateLimit / 8),
        search_calls: searchCalls,
        max_ai_candidates: candidateLimit,
        remaining_credits: 100,
        estimate_basis: "mock：依查詢組數與候選上限估算，預覽不扣點",
        expires_at: nanoNow() + 10 * 60 * 1_000_000_000,
      };
      return preview;
    },
  };
}
