/**
 * demand-radar live repositories（radar／crm）。
 *
 * 這裡刻意不做任何降級：後端尚未實作的能力回 501010，`apiRequest` 會丟 ApiError，
 * 頁面必須顯示錯誤而不是空清單。新頁樣式與文案不在此檔。
 */
import type {
  Contact,
  ContactDetail,
  ContactStageCount,
  ContactTouch,
  CostPreview,
  DemandMap,
  DemandMapPhrase,
  CrmStats,
  FollowUp,
  ManualImportResult,
  Opportunity,
  ProductFitReason,
  ProductMatch,
  RadarSweep,
  RadarToday,
  RadarWatch,
  ReplyVariant,
  ServiceProfile,
  WatchTermSuggestion,
} from "../../domain/types";
import type { CrmRepo, RadarRepo } from "../repos";
import { apiRequest } from "./http";

type Raw = Record<string, unknown>;

function str(v: unknown): string {
  return typeof v === "string" ? v : v == null ? "" : String(v);
}

function optStr(v: unknown): string | undefined {
  const s = str(v);
  return s === "" ? undefined : s;
}

function num(v: unknown): number {
  return Number(v ?? 0) || 0;
}

function optNum(v: unknown): number | undefined {
  if (v == null) return undefined;
  const n = Number(v);
  return Number.isFinite(n) && n !== 0 ? n : undefined;
}

function strList(v: unknown): string[] {
  return Array.isArray(v) ? v.map(str) : [];
}

function rawList(v: unknown): Raw[] {
  return Array.isArray(v) ? (v as Raw[]) : [];
}

function total(raw: Raw): number {
  const pagination = (raw.pagination ?? {}) as Raw;
  return num(pagination.total);
}

function query(params: Record<string, string | number | boolean | undefined>): string {
  const parts: string[] = [];
  for (const [k, v] of Object.entries(params)) {
    if (v === undefined || v === "") continue;
    parts.push(`${k}=${encodeURIComponent(String(v))}`);
  }
  return parts.length ? `?${parts.join("&")}` : "";
}

function mapServiceProfile(raw: Raw): ServiceProfile {
  return {
    exists: Boolean(raw.exists),
    services: rawList(raw.services).map((s) => ({
      name: str(s.name),
      price_min: optNum(s.price_min),
      price_max: optNum(s.price_max),
      currency: optStr(s.currency),
    })),
    cases: rawList(raw.cases).map((c) => ({
      title: str(c.title),
      summary: optStr(c.summary),
      link: optStr(c.link),
    })),
    forbidden: strList(raw.forbidden),
    faq: rawList(raw.faq).map((f) => ({ question: str(f.question), answer: str(f.answer) })),
    service_areas: strList(raw.service_areas),
    remote_ok: Boolean(raw.remote_ok),
    availability: optStr(raw.availability),
    tone_note: optStr(raw.tone_note),
    updated_at: optNum(raw.updated_at),
  };
}

function mapWatch(raw: Raw): RadarWatch {
  return {
    id: str(raw.id),
    terms: strList(raw.terms),
    exclude_terms: strList(raw.exclude_terms),
    regions: strList(raw.regions),
    status: (str(raw.status) || "active") as RadarWatch["status"],
    context_mode: (str(raw.context_mode) || "generic") as RadarWatch["context_mode"],
    brand_id: optStr(raw.brand_id),
    product_id: optStr(raw.product_id),
    brand_name_snapshot: optStr(raw.brand_name_snapshot),
    product_label_snapshot: optStr(raw.product_label_snapshot),
    context_bound_at: optNum(raw.context_bound_at),
    pause_reason: optStr(raw.pause_reason) as RadarWatch["pause_reason"],
    last_swept_at: optNum(raw.last_swept_at),
    created_at: num(raw.created_at),
    updated_at: num(raw.updated_at),
    first_sweep_triggered: Boolean(raw.first_sweep_triggered),
  };
}

function mapProductMatch(raw: Raw): ProductMatch {
  return {
    brand_id: str(raw.brand_id),
    product_id: str(raw.product_id),
    brand_name_snapshot: str(raw.brand_name_snapshot),
    product_label_snapshot: str(raw.product_label_snapshot),
    brand_updated_at: num(raw.brand_updated_at),
    product_updated_at: num(raw.product_updated_at),
    product_fit_score: num(raw.product_fit_score),
    product_fit_band: (str(raw.product_fit_band) || "weak") as ProductMatch["product_fit_band"],
    eligible: Boolean(raw.eligible),
    excluded: Boolean(raw.excluded),
    exclude_reason: optStr(raw.exclude_reason),
    reasons: rawList(raw.reasons).map(
      (r): ProductFitReason => ({
        dimension: str(r.dimension) as ProductFitReason["dimension"],
        score: num(r.score),
        reason: str(r.reason),
        candidate_excerpt: optStr(r.candidate_excerpt),
        product_basis: optStr(r.product_basis),
      }),
    ),
    risks: strList(raw.risks),
    watch_ids: strList(raw.watch_ids),
    matched_terms: strList(raw.matched_terms),
    matched_at: num(raw.matched_at),
  };
}

function mapReply(raw: Raw): ReplyVariant {
  return {
    id: str(raw.id),
    opportunity_id: str(raw.opportunity_id),
    variant: str(raw.variant) as ReplyVariant["variant"],
    text: str(raw.text),
    used_at: optNum(raw.used_at),
    sent_channel: optStr(raw.sent_channel) as ReplyVariant["sent_channel"],
    outbox_id: optStr(raw.outbox_id),
    created_at: num(raw.created_at),
  };
}

function mapOpportunity(raw: Raw): Opportunity {
  const override = raw.override as Raw | null | undefined;
  const defaultReply = raw.default_reply as Raw | null | undefined;
  return {
    id: str(raw.id),
    watch_id: optStr(raw.watch_id),
    source: (str(raw.source) || "threads") as Opportunity["source"],
    source_scout_post_id: optStr(raw.source_scout_post_id),
    external_id: str(raw.external_id),
    permalink: str(raw.permalink),
    author_handle: str(raw.author_handle),
    text: str(raw.text),
    posted_at: num(raw.posted_at),
    status: str(raw.status) as Opportunity["status"],
    intent_score: num(raw.intent_score),
    intent_band: (str(raw.intent_band) || "low") as Opportunity["intent_band"],
    // reasons 缺省時給空陣列，避免卡片展開判定理由時 .map 炸頁。
    reasons: rawList(raw.reasons).map((r) => ({
      dimension: str(r.dimension) as Opportunity["reasons"][number]["dimension"],
      score: num(r.score),
      reason: str(r.reason),
    })),
    region_detected: optStr(raw.region_detected),
    region_match: (str(raw.region_match) || "unknown") as Opportunity["region_match"],
    freshness_hours: num(raw.freshness_hours),
    matched_service: optStr(raw.matched_service),
    matched_terms: strList(raw.matched_terms),
    reject_reason: optStr(raw.reject_reason),
    override: override
      ? {
          from_band: optStr(override.from_band),
          to_band: optStr(override.to_band),
          from_status: optStr(override.from_status),
          to_status: optStr(override.to_status),
          actor_uid: num(override.actor_uid),
          at: num(override.at),
        }
      : undefined,
    contact_id: optStr(raw.contact_id),
    default_reply: defaultReply ? mapReply(defaultReply) : undefined,
    primary_brand_id: optStr(raw.primary_brand_id),
    primary_product_id: optStr(raw.primary_product_id),
    primary_brand_name: optStr(raw.primary_brand_name),
    primary_product_label: optStr(raw.primary_product_label),
    primary_product_fit_score: optNum(raw.primary_product_fit_score),
    primary_product_fit_band: optStr(raw.primary_product_fit_band) as Opportunity["primary_product_fit_band"],
    primary_product_overridden: Boolean(raw.primary_product_overridden),
    product_matches: rawList(raw.product_matches).map(mapProductMatch),
    review_state: (str(raw.review_state) || (str(raw.status) === "accepted" ? "completed" : str(raw.status) === "dismissed" ? "removed" : "pending")) as Opportunity["review_state"],
    previous_review_state: optStr(raw.previous_review_state) as Opportunity["previous_review_state"],
    removal_reason: optStr(raw.removal_reason) as Opportunity["removal_reason"],
    removal_note: optStr(raw.removal_note),
    removed_at: optNum(raw.removed_at),
    removed_by: optNum(raw.removed_by),
    last_matched_at: optNum(raw.last_matched_at),
    priority_score: optNum(raw.priority_score),
    priority_band: optStr(raw.priority_band) as Opportunity["priority_band"],
    priority_factors: raw.priority_factors
      ? {
          pain_fit: num((raw.priority_factors as Raw).pain_fit),
          demand_intent: num((raw.priority_factors as Raw).demand_intent),
          evidence_quality: num((raw.priority_factors as Raw).evidence_quality),
          freshness: num((raw.priority_factors as Raw).freshness),
        }
      : raw.pain_fit_score != null || raw.demand_intent_score != null
        ? {
            pain_fit: num(raw.pain_fit_score),
            demand_intent: num(raw.demand_intent_score),
            evidence_quality: num(raw.evidence_quality_score),
            freshness: num(raw.freshness_score),
          }
        : undefined,
    demand_evidence: strList(raw.demand_evidence),
    demand_input_version: optStr(raw.demand_input_version),
    demand_map_version: optNum(raw.demand_map_version),
    created_at: num(raw.created_at),
  };
}

function mapDemandPhrase(raw: Raw): DemandMapPhrase {
  return {
    text: str(raw.text),
    kind: str(raw.kind),
    basis_kind: optStr(raw.basis_kind),
    basis_text: optStr(raw.basis_text),
    origin: str(raw.origin) || "product",
    enabled: raw.enabled !== false,
  };
}

function mapDemandMap(raw: Raw): DemandMap {
  return {
    product_id: str(raw.product_id),
    demand_input_version: str(raw.demand_input_version),
    map_version: num(raw.map_version),
    state: str(raw.state) || "incomplete",
    pain_phrases: rawList(raw.pain_phrases).map(mapDemandPhrase),
    scenario_phrases: rawList(raw.scenario_phrases).map(mapDemandPhrase),
    desired_outcomes: rawList(raw.desired_outcomes).map(mapDemandPhrase),
    solution_signals: rawList(raw.solution_signals).map(mapDemandPhrase),
    exclusion_signals: rawList(raw.exclusion_signals).map(mapDemandPhrase),
    source_basis: strList(raw.source_basis),
    custom_phrases: rawList(raw.custom_phrases).map(mapDemandPhrase),
    ai_enriched_at: optNum(raw.ai_enriched_at),
    updated_at: num(raw.updated_at),
  };
}

function mapCostPreview(raw: Raw): CostPreview {
  return {
    preview_id: str(raw.preview_id),
    action: str(raw.action),
    key_mode: str(raw.key_mode),
    fixed_credits: num(raw.fixed_credits),
    min_credits: num(raw.min_credits),
    max_credits: num(raw.max_credits),
    search_calls: num(raw.search_calls),
    max_ai_candidates: num(raw.max_ai_candidates),
    remaining_credits: num(raw.remaining_credits),
    estimate_basis: str(raw.estimate_basis),
    expires_at: num(raw.expires_at),
  };
}

function mapSweep(raw: Raw): RadarSweep {
  return {
    id: str(raw.id),
    watch_id: str(raw.watch_id),
    job_id: optStr(raw.job_id),
    path: (str(raw.path) || "api") as RadarSweep["path"],
    hit_count: num(raw.hit_count),
    judged_count: num(raw.judged_count),
    created_count: num(raw.created_count),
    truncated_count: num(raw.truncated_count),
    match_evaluated_count: num(raw.match_evaluated_count),
    match_merged_count: num(raw.match_merged_count),
    fit_rejected_count: num(raw.fit_rejected_count),
    failed_reason: optStr(raw.failed_reason),
    credits_used: num(raw.credits_used),
    started_at: num(raw.started_at),
    ended_at: optNum(raw.ended_at),
    demand_input_version: optStr(raw.demand_input_version),
    demand_map_version: optNum(raw.demand_map_version),
    deduped_count: num(raw.deduped_count),
    prefilter_pass_count: num(raw.prefilter_pass_count),
    prefilter_review_count: num(raw.prefilter_review_count),
    prefilter_rejected_count: num(raw.prefilter_rejected_count),
    cached_judgment_count: num(raw.cached_judgment_count),
    tombstone_matched_count: num(raw.tombstone_matched_count),
    budget_deferred_count: num(raw.budget_deferred_count),
    sweep_status: optStr(raw.status),
    credit_search: num(raw.credit_search),
    credit_demand_map: num(raw.credit_demand_map),
    credit_judge: num(raw.credit_judge),
    credit_reply: num(raw.credit_reply),
  };
}

function mapContact(raw: Raw): Contact {
  return {
    id: str(raw.id),
    source_platform: str(raw.source_platform),
    author_handle: str(raw.author_handle),
    display_name: optStr(raw.display_name),
    stage: str(raw.stage) as Contact["stage"],
    needs_follow_up: Boolean(raw.needs_follow_up),
    follow_up_days: num(raw.follow_up_days),
    last_touch_at: optNum(raw.last_touch_at),
    opportunity_ids: strList(raw.opportunity_ids),
    opportunity_count: num(raw.opportunity_count),
    merged_from: Array.isArray(raw.merged_from) ? strList(raw.merged_from) : undefined,
    top_intent_band: optStr(raw.top_intent_band) as Contact["top_intent_band"],
    top_intent_score: optNum(raw.top_intent_score),
    created_at: num(raw.created_at),
    updated_at: num(raw.updated_at),
  };
}

function mapTouch(raw: Raw): ContactTouch {
  return {
    id: str(raw.id),
    contact_id: str(raw.contact_id),
    type: str(raw.type) as ContactTouch["type"],
    from_stage: optStr(raw.from_stage) as ContactTouch["from_stage"],
    to_stage: optStr(raw.to_stage) as ContactTouch["to_stage"],
    body: optStr(raw.body),
    brand_id: optStr(raw.brand_id),
    product_id: optStr(raw.product_id),
    product_label_snapshot: optStr(raw.product_label_snapshot),
    opportunity_id: optStr(raw.opportunity_id),
    actor_uid: num(raw.actor_uid),
    created_at: num(raw.created_at),
  };
}

function mapFollowUp(raw: Raw): FollowUp {
  const contact = (raw.contact ?? {}) as Raw;
  return {
    id: str(raw.id),
    contact_id: str(raw.contact_id),
    due_at: num(raw.due_at),
    status: str(raw.status) as FollowUp["status"],
    notified_count: num(raw.notified_count),
    contact: {
      id: str(contact.id),
      source_platform: str(contact.source_platform),
      author_handle: str(contact.author_handle),
      display_name: optStr(contact.display_name),
      stage: str(contact.stage) as Contact["stage"],
    },
    created_at: num(raw.created_at),
  };
}

const RADAR_BASE = "/api/v1/radar";
const CRM_BASE = "/api/v1/crm";

export function createLiveRadarRepo(): RadarRepo {
  return {
    async getServiceProfile() {
      return mapServiceProfile(await apiRequest<Raw>(`${RADAR_BASE}/service-profile`));
    },
    async saveServiceProfile(patch) {
      const raw = await apiRequest<Raw>(`${RADAR_BASE}/service-profile`, {
        method: "PUT",
        body: patch,
      });
      return mapServiceProfile(raw);
    },
    async listWatches(page = 1, pageSize = 20, status, contextMode, brandId, productId) {
      const raw = await apiRequest<Raw>(
        `${RADAR_BASE}/watches${query({ page, pageSize, status, context_mode: contextMode, brand_id: brandId, product_id: productId })}`,
      );
      return {
        list: rawList(raw.list).map(mapWatch),
        total: total(raw),
        active_count: num(raw.active_count),
        max_active: num(raw.max_active),
        profile_exists: Boolean(raw.profile_exists),
      };
    },
    async createWatch(input) {
      return mapWatch(await apiRequest<Raw>(`${RADAR_BASE}/watches`, { method: "POST", body: input }));
    },
    async assignWatchProduct(id, brandId, productId) {
      const raw = await apiRequest<Raw>(`${RADAR_BASE}/watches/${encodeURIComponent(id)}/assign-product`, {
        method: "POST",
        body: { brand_id: brandId, product_id: productId },
      });
      return mapWatch(raw);
    },
    async updateWatch(id, patch) {
      const raw = await apiRequest<Raw>(`${RADAR_BASE}/watches/${encodeURIComponent(id)}`, {
        method: "PUT",
        body: patch,
      });
      return mapWatch(raw);
    },
    async pauseWatch(id) {
      const raw = await apiRequest<Raw>(`${RADAR_BASE}/watches/${encodeURIComponent(id)}/pause`, {
        method: "POST",
        body: {},
      });
      return mapWatch(raw);
    },
    async resumeWatch(id) {
      const raw = await apiRequest<Raw>(`${RADAR_BASE}/watches/${encodeURIComponent(id)}/resume`, {
        method: "POST",
        body: {},
      });
      return mapWatch(raw);
    },
    async archiveWatch(id) {
      await apiRequest<Raw>(`${RADAR_BASE}/watches/${encodeURIComponent(id)}`, { method: "DELETE" });
    },
    async deleteArchivedWatch(id) {
      await apiRequest<Raw>(`${RADAR_BASE}/watches/${encodeURIComponent(id)}/purge`, { method: "DELETE" });
    },
    async suggestWatchTerms(input) {
      const body = typeof input === "number" ? { limit: input } : input ?? {};
      const raw = await apiRequest<Raw>(`${RADAR_BASE}/watches/suggest`, {
        method: "POST",
        body,
      });
      return rawList(raw.list).map(
        (s): WatchTermSuggestion => ({
          term: str(s.term),
          reason: str(s.reason),
          usage: str(s.usage) === "exclude" ? "exclude" : "include",
          basis_kind: optStr(s.basis_kind) as WatchTermSuggestion["basis_kind"],
          basis_text: optStr(s.basis_text),
        }),
      );
    },
    async triggerWatchSweep(id) {
      const raw = await apiRequest<Raw>(`${RADAR_BASE}/watches/${encodeURIComponent(id)}/sweep`, {
        method: "POST",
        body: {},
      });
      return { job_id: str(raw.job_id), sweep_id: optStr(raw.sweep_id) };
    },
    async getToday(filter = {}) {
      const raw = await apiRequest<Raw>(`${RADAR_BASE}/today${query({
        brand_id: filter.brand_id,
        product_id: filter.product_id,
        fit_band: filter.fit_band,
      })}`);
      const stats = (raw.stats ?? {}) as Raw;
      return {
        stats: {
          total: num(stats.total),
          high: num(stats.high),
          mid: num(stats.mid),
          low: num(stats.low),
        },
        high: rawList(raw.high).map(mapOpportunity),
        mid: rawList(raw.mid).map(mapOpportunity),
        low: rawList(raw.low).map(mapOpportunity),
        truncated_count: num(raw.truncated_count),
        last_swept_at: optNum(raw.last_swept_at),
        empty_reason: optStr(raw.empty_reason) as RadarToday["empty_reason"],
        empty_hint: optStr(raw.empty_hint),
      };
    },
    async listOpportunities(filter = {}) {
      const raw = await apiRequest<Raw>(
        `${RADAR_BASE}/opportunities${query({
          page: filter.page ?? 1,
          pageSize: filter.pageSize ?? 20,
          band: filter.band,
          status: filter.status,
          watch_id: filter.watch_id,
          brand_id: filter.brand_id,
          product_id: filter.product_id,
          fit_band: filter.fit_band,
          match_state: filter.match_state,
          review_state: filter.review_state,
          time_scope: filter.time_scope,
          priority_band: filter.priority_band,
          sort: filter.sort,
          from: filter.from,
          to: filter.to,
        })}`,
      );
      return { list: rawList(raw.list).map(mapOpportunity), total: total(raw) };
    },
    async getOpportunity(id) {
      return mapOpportunity(
        await apiRequest<Raw>(`${RADAR_BASE}/opportunities/${encodeURIComponent(id)}`),
      );
    },
    async acceptOpportunity(id) {
      const raw = await apiRequest<Raw>(
        `${RADAR_BASE}/opportunities/${encodeURIComponent(id)}/accept`,
        { method: "POST", body: {} },
      );
      return {
        opportunity_id: str(raw.opportunity_id),
        contact_id: optStr(raw.contact_id),
        status: str(raw.status),
      };
    },
    async dismissOpportunity(id, reason) {
      const raw = await apiRequest<Raw>(
        `${RADAR_BASE}/opportunities/${encodeURIComponent(id)}/dismiss`,
        { method: "POST", body: reason ? { reason } : {} },
      );
      return mapOpportunity(raw);
    },
    async updateOpportunityReviewState(id, patch) {
      const raw = await apiRequest<Raw>(`${RADAR_BASE}/opportunities/${encodeURIComponent(id)}/review-state`, {
        method: "PUT",
        body: patch,
      });
      return mapOpportunity(raw);
    },
    async overrideOpportunity(id, patch) {
      const raw = await apiRequest<Raw>(
        `${RADAR_BASE}/opportunities/${encodeURIComponent(id)}/override`,
        { method: "POST", body: patch },
      );
      return mapOpportunity(raw);
    },
    async setPrimaryProduct(id, productId, reason) {
      const raw = await apiRequest<Raw>(`${RADAR_BASE}/opportunities/${encodeURIComponent(id)}/primary-product`, {
        method: "PUT",
        body: { product_id: productId, reason },
      });
      return mapOpportunity(raw);
    },
    async listReplies(opportunityId) {
      const raw = await apiRequest<Raw>(
        `${RADAR_BASE}/opportunities/${encodeURIComponent(opportunityId)}/replies`,
      );
      return rawList(raw.list).map(mapReply);
    },
    async createReply(opportunityId, variant) {
      const raw = await apiRequest<Raw>(
        `${RADAR_BASE}/opportunities/${encodeURIComponent(opportunityId)}/replies`,
        { method: "POST", body: { variant } },
      );
      return mapReply(raw);
    },
    async markReplyUsed(opportunityId, replyId, channel, accountId) {
      const raw = await apiRequest<Raw>(
        `${RADAR_BASE}/opportunities/${encodeURIComponent(opportunityId)}/replies/${encodeURIComponent(replyId)}/mark-used`,
        { method: "POST", body: { channel, account_id: accountId } },
      );
      const replyRaw = (raw.reply ?? raw) as Raw;
      return {
        reply: mapReply(replyRaw),
        health_advice: optStr(raw.health_advice),
      };
    },
    async listSweeps(page = 1, pageSize = 20, watchId) {
      const raw = await apiRequest<Raw>(
        `${RADAR_BASE}/sweeps${query({ page, pageSize, watch_id: watchId })}`,
      );
      return { list: rawList(raw.list).map(mapSweep), total: total(raw) };
    },
    async importOpportunities(items, context) {
      const raw = await apiRequest<Raw>(`${RADAR_BASE}/import`, {
        method: "POST",
        body: {
          items: items.map((it) => ({
            url: it.url,
            text: it.text,
            author: it.author,
            posted_at: it.posted_at,
          })),
          ...context,
        },
      });
      const results = rawList(raw.results).map(
        (r): ManualImportResult => ({
          url: str(r.url),
          opportunity_id: optStr(r.opportunity_id),
          status: str(r.status) as ManualImportResult["status"],
          intent_band: optStr(r.intent_band) as ManualImportResult["intent_band"],
          intent_score: optNum(r.intent_score),
          error: optStr(r.error),
        }),
      );
      return { results };
    },
    async exploreOpportunities(terms, context) {
      const raw = await apiRequest<Raw>(`${RADAR_BASE}/explore`, {
        method: "POST",
        body: { terms, ...context },
      });
      return {
        sweep_id: str(raw.sweep_id),
        hit_count: num(raw.hit_count),
        judged_count: num(raw.judged_count),
        created_count: num(raw.created_count),
        matched_count: num(raw.matched_count),
        merged_count: num(raw.merged_count),
        truncated_count: num(raw.truncated_count),
        credits_used: num(raw.credits_used),
        deduped_count: num(raw.deduped_count),
        prefilter_pass_count: num(raw.prefilter_pass_count),
        prefilter_review_count: num(raw.prefilter_review_count),
        prefilter_rejected_count: num(raw.prefilter_rejected_count),
        cached_judgment_count: num(raw.cached_judgment_count),
        tombstone_matched_count: num(raw.tombstone_matched_count),
        budget_deferred_count: num(raw.budget_deferred_count),
        sweep_status: optStr(raw.status),
      };
    },
    async getDemandMap(productId) {
      return mapDemandMap(await apiRequest<Raw>(`${RADAR_BASE}/products/${encodeURIComponent(productId)}/demand-map`));
    },
    async updateDemandMap(productId, patch) {
      return mapDemandMap(await apiRequest<Raw>(`${RADAR_BASE}/products/${encodeURIComponent(productId)}/demand-map`, {
        method: "PUT",
        body: patch,
      }));
    },
    async enrichDemandMap(productId, input) {
      return mapDemandMap(await apiRequest<Raw>(`${RADAR_BASE}/products/${encodeURIComponent(productId)}/demand-map/enrich`, {
        method: "POST",
        body: input,
      }));
    },
    async getCostPreview(input) {
      return mapCostPreview(await apiRequest<Raw>(`${RADAR_BASE}/cost-preview${query(input)}`));
    },
  };
}

export function createLiveCrmRepo(): CrmRepo {
  return {
    async listContacts(filter = {}) {
      const raw = await apiRequest<Raw>(
        `${CRM_BASE}/contacts${query({
          page: filter.page ?? 1,
          pageSize: filter.pageSize ?? 20,
          query: filter.query,
          stage: filter.stage,
          follow_up: filter.follow_up === undefined ? undefined : String(filter.follow_up),
          band: filter.band,
          sort: filter.sort,
        })}`,
      );
      return {
        list: rawList(raw.list).map(mapContact),
        total: total(raw),
        stage_counts: rawList(raw.stage_counts).map(
          (s): ContactStageCount => ({
            stage: str(s.stage) as ContactStageCount["stage"],
            count: num(s.count),
          }),
        ),
      };
    },
    async getContact(id, page = 1, pageSize = 20) {
      const raw = await apiRequest<Raw>(
        `${CRM_BASE}/contacts/${encodeURIComponent(id)}${query({ page, pageSize })}`,
      );
      return {
        contact: mapContact((raw.contact ?? {}) as Raw),
        touches: rawList(raw.touches).map(mapTouch),
        opportunities: rawList(raw.opportunities).map((o) => ({
          id: str(o.id),
          permalink: str(o.permalink),
          text: str(o.text),
          intent_score: num(o.intent_score),
          intent_band: (str(o.intent_band) || "low") as ContactDetail["opportunities"][number]["intent_band"],
          created_at: num(o.created_at),
          primary_brand_name: optStr(o.primary_brand_name),
          primary_product_label: optStr(o.primary_product_label),
        })),
      };
    },
    async deleteContact(id) {
      await apiRequest<Raw>(`${CRM_BASE}/contacts/${encodeURIComponent(id)}`, {
        method: "DELETE",
      });
    },
    async updateStage(id, stage, note) {
      const raw = await apiRequest<Raw>(`${CRM_BASE}/contacts/${encodeURIComponent(id)}/stage`, {
        method: "POST",
        body: note ? { stage, note } : { stage },
      });
      return mapContact(raw);
    },
    async setFollowUp(id, needsFollowUp, days) {
      const raw = await apiRequest<Raw>(
        `${CRM_BASE}/contacts/${encodeURIComponent(id)}/follow-up`,
        { method: "POST", body: days ? { needs_follow_up: needsFollowUp, days } : { needs_follow_up: needsFollowUp } },
      );
      return mapContact(raw);
    },
    async addNote(id, body) {
      const raw = await apiRequest<Raw>(`${CRM_BASE}/contacts/${encodeURIComponent(id)}/notes`, {
        method: "POST",
        body: { body },
      });
      return mapTouch(raw);
    },
    async mergeContact(id, sourceContactId) {
      const raw = await apiRequest<Raw>(`${CRM_BASE}/contacts/${encodeURIComponent(id)}/merge`, {
        method: "POST",
        body: { source_contact_id: sourceContactId },
      });
      return mapContact(raw);
    },
    async unmergeContact(id, mergedContactId) {
      const raw = await apiRequest<Raw>(`${CRM_BASE}/contacts/${encodeURIComponent(id)}/unmerge`, {
        method: "POST",
        body: { merged_contact_id: mergedContactId },
      });
      return mapContact(raw);
    },
    async reportConversion(id, input) {
      const raw = await apiRequest<Raw>(
        `${CRM_BASE}/contacts/${encodeURIComponent(id)}/conversion`,
        { method: "POST", body: input },
      );
      return { contact_id: str(raw.contact_id), outcome_id: optStr(raw.outcome_id), stage: str(raw.stage) };
    },
    async updateConversion(id, input) {
      const raw = await apiRequest<Raw>(
        `${CRM_BASE}/contacts/${encodeURIComponent(id)}/conversion`,
        { method: "PUT", body: input },
      );
      return { contact_id: str(raw.contact_id), outcome_id: optStr(raw.outcome_id), stage: str(raw.stage) };
    },
    async deleteConversion(id) {
      await apiRequest<Raw>(`${CRM_BASE}/contacts/${encodeURIComponent(id)}/conversion`, {
        method: "DELETE",
      });
    },
    async listFollowUps(page = 1, pageSize = 20, status) {
      const raw = await apiRequest<Raw>(`${CRM_BASE}/followups${query({ page, pageSize, status })}`);
      return { list: rawList(raw.list).map(mapFollowUp), total: total(raw) };
    },
    async doneFollowUp(id) {
      const raw = await apiRequest<Raw>(`${CRM_BASE}/followups/${encodeURIComponent(id)}/done`, {
        method: "POST",
        body: {},
      });
      return mapFollowUp(raw);
    },
    async snoozeFollowUp(id, days) {
      const raw = await apiRequest<Raw>(`${CRM_BASE}/followups/${encodeURIComponent(id)}/snooze`, {
        method: "POST",
        body: { days },
      });
      return mapFollowUp(raw);
    },
    async generateFollowUpMessage(id) {
      const raw = await apiRequest<Raw>(`${CRM_BASE}/followups/${encodeURIComponent(id)}/message`, {
        method: "POST",
        body: {},
      });
      return str(raw.text);
    },
    async getStats(from, to) {
      const raw = await apiRequest<Raw>(`${CRM_BASE}/stats${query({ from, to })}`);
      return {
        terms: rawList(raw.terms).map((t) => ({
          term: str(t.term),
          accepted: num(t.accepted),
          replied: num(t.replied),
          won: num(t.won),
          conversion_rate: optNum(t.conversion_rate),
          insufficient_sample: Boolean(t.insufficient_sample),
        })),
        variants: rawList(raw.variants).map((v) => ({
          variant: str(v.variant) as CrmStats["variants"][number]["variant"],
          used: num(v.used),
          replied: num(v.replied),
          won: num(v.won),
          success_rate: optNum(v.success_rate),
          insufficient_sample: Boolean(v.insufficient_sample),
        })),
        sources: rawList(raw.sources).map((s) => ({
          source: str(s.source) as CrmStats["sources"][number]["source"],
          won: num(s.won),
          insufficient_sample: Boolean(s.insufficient_sample),
        })),
        unavailable_dimensions: (Array.isArray(raw.unavailable_dimensions)
          ? raw.unavailable_dimensions.map(str)
          : []) as CrmStats["unavailable_dimensions"],
      };
    },
  };
}
