/**
 * Live repository implementations — 主路徑一律走 gateway。
 */

import type {
  AiSettings,
  AppNotification,
  ExternalThreadTarget,
  Job,
  Member,
  MentionItem,
  OwnPost,
  OutboxBundle,
  Persona,
  PlacementSettings,
  ThreadPlay,
  ThreadsAccount,
  TokenPair,
  ViralAnalysis,
} from "../../domain/types";
import type { MemberAdminView } from "../../lib/tenantUsers";
import type { PlanPurchase } from "../../lib/planPurchase";
import type {
  PlanId,
  UsageEvent,
  UsageKeyMode,
	UsageGranularity,
  UsageMemberPrefs,
  UsageMeter,
  UsageMonthSummary,
	TenantUsageAnalytics,
} from "../../lib/usageMeter";
import { PLANS } from "../../lib/usageMeter";
import type {
  AccountsRepo,
  AdminCreateMemberInput,
  AdminUsersRepo,
  AuthRepo,
  ComposeRepo,
  JobsRepo,
  MemberProfilePatch,
  MentionsRepo,
  NotificationsRepo,
  OutboxRepo,
  OwnPostsRepo,
  PersonasRepo,
  PlaysRepo,
  Repos,
  SettingsRepo,
  ThreadsPlatformStatus,
  UsageRepo,
  BillingCheckoutSession,
  BillingCheckoutStatus,
  BillingSubscription,
} from "../repos";
import {
  createLiveInspiration,
  createLiveMedia,
  createLiveResearch,
  createLiveScout,
} from "./m5Repos";
import { createLiveInviteRepo } from "./inviteRepo";
import { createLiveGrowthRepo } from "./growthRepo";
import { createLiveCrmRepo, createLiveRadarRepo } from "./radarRepos";
import { ApiError, apiRequest, loadTokens, saveTokens, type StoredTokens } from "./http";

function uidToString(uid: unknown): string {
  if (typeof uid === "number" || typeof uid === "bigint") return String(uid);
  if (typeof uid === "string") return uid;
  return String(uid ?? "");
}

function billingPlanId(value: unknown): PlanId {
  const id = String(value ?? "free");
  return id === "starter" || id === "pro" ? id : "free";
}

export function mapBillingCheckoutSession(raw: Record<string, unknown>): BillingCheckoutSession {
  return {
    id: String(raw.id ?? ""),
    session_id: String(raw.session_id ?? ""),
    url: String(raw.url ?? ""),
    expires_at: Number(raw.expires_at ?? 0),
  };
}

export function mapBillingCheckoutStatus(raw: Record<string, unknown>): BillingCheckoutStatus {
  return {
    id: String(raw.id ?? ""),
    plan_id: billingPlanId(raw.plan_id),
    checkout_status: String(raw.checkout_status ?? raw.status ?? ""),
    payment_status: String(raw.payment_status ?? ""),
    fulfillment_status: String(raw.fulfillment_status ?? ""),
  };
}

export function mapBillingSubscription(raw: Record<string, unknown>): BillingSubscription {
  return {
    plan_id: billingPlanId(raw.plan_id),
    status: String(raw.status ?? ""),
    current_period_end: Number(raw.current_period_end ?? 0),
    cancel_at_period_end: Boolean(raw.cancel_at_period_end),
  };
}

function normalizeMember(raw: Record<string, unknown>): Member {
  const identitiesRaw = Array.isArray(raw.identities) ? raw.identities : [];
  return {
    tenant_id: String(raw.tenant_id ?? "default"),
    uid: uidToString(raw.uid),
    email: String(raw.email ?? ""),
    phone: raw.phone != null ? String(raw.phone) : undefined,
    display_name: String(raw.display_name ?? ""),
    nickname: raw.nickname != null ? String(raw.nickname) : undefined,
    full_name: raw.full_name != null ? String(raw.full_name) : undefined,
    roles: (raw.roles as Member["roles"]) ?? ["member"],
    status: (raw.status as Member["status"]) ?? "active",
    email_verified: Boolean(raw.email_verified),
    email_verified_at: (raw.email_verified_at as number | null | undefined) ?? null,
    phone_verified: raw.phone_verified != null ? Boolean(raw.phone_verified) : undefined,
    phone_verified_at: (raw.phone_verified_at as number | null | undefined) ?? null,
    invite_code: raw.invite_code != null ? String(raw.invite_code) : undefined,
    parent_uid:
      raw.parent_uid === null || raw.parent_uid === undefined || raw.parent_uid === 0
        ? null
        : uidToString(raw.parent_uid),
    bio: raw.bio != null ? String(raw.bio) : undefined,
    timezone: raw.timezone != null ? String(raw.timezone) : undefined,
    avatar_url: (() => {
      if (raw.avatar_url === null || raw.avatar_url === undefined) return null;
      const s = String(raw.avatar_url).trim();
      return s === "" ? null : s;
    })(),
    notify_email: Boolean(raw.notify_email),
    gender_code: raw.gender_code != null ? Number(raw.gender_code) : undefined,
    birthdate: raw.birthdate != null ? Number(raw.birthdate) : undefined,
    national: raw.national != null ? String(raw.national) : undefined,
    address: raw.address != null ? String(raw.address) : undefined,
    post_code: raw.post_code != null ? String(raw.post_code) : undefined,
    preferred_language:
      raw.preferred_language != null ? String(raw.preferred_language) : undefined,
    currency: raw.currency != null ? String(raw.currency) : undefined,
    onboarding_status:
      raw.onboarding_status != null ? String(raw.onboarding_status) : undefined,
    onboarding_done_at:
      raw.onboarding_done_at != null ? Number(raw.onboarding_done_at) : undefined,
    identities: identitiesRaw.map((it) => {
      const row = it as Record<string, unknown>;
      return {
        login_id: String(row.login_id ?? ""),
        platform: String(row.platform ?? ""),
        account_type: String(row.account_type ?? ""),
        created_at: row.created_at != null ? Number(row.created_at) : undefined,
      };
    }),
  };
}

function normalizeAdmin(raw: Record<string, unknown>): MemberAdminView {
  const m = normalizeMember(raw);
  return {
    uid: m.uid,
    email: m.email,
    display_name: m.display_name,
    roles: m.roles,
    status: m.status ?? "active",
    bio: m.bio,
    timezone: m.timezone,
    notify_email: m.notify_email,
    avatar_url: m.avatar_url,
    email_verified: m.email_verified,
    email_verified_at: m.email_verified_at ?? null,
    invite_code: m.invite_code ?? "",
    parent_uid: m.parent_uid ?? null,
    created_at: Number(raw.created_at ?? raw.joined_at ?? 0),
    updated_at: Number(raw.updated_at ?? raw.created_at ?? 0),
    onboarding_status: m.onboarding_status,
    onboarding_done_at: m.onboarding_done_at,
  };
}

function createLiveAuth(): AuthRepo {
  return {
    async login(email, password) {
      const data = await apiRequest<{ tokens: TokenPair; member: Record<string, unknown> }>(
        "/api/v1/auth/login",
        { method: "POST", body: { email, password }, auth: false },
      );
      saveTokens(data.tokens as StoredTokens);
      return { tokens: data.tokens, member: normalizeMember(data.member) };
    },
    async logout() {
      const tok = loadTokens();
      try {
        await apiRequest("/api/v1/auth/logout", {
          method: "POST",
          body: { refresh_token: tok?.refresh_token ?? "" },
          auth: false,
        });
      } catch {
        /* still clear local */
      }
      saveTokens(null);
    },
    async me() {
      const tok = loadTokens();
      if (!tok?.access_token) return null;
      try {
        const raw = await apiRequest<Record<string, unknown>>("/api/v1/auth/me");
        return normalizeMember(raw);
      } catch (error) {
        if (error instanceof ApiError && error.httpStatus === 401 && !error.sessionUnknown) {
          saveTokens(null);
          return null;
        }
        throw error;
      }
    },
    async updateProfile(patch: MemberProfilePatch) {
      // undefined = 省略（不改）；null = 清除（avatar_url 送 ""，後端 *string 可區分）
      // 其餘 null 仍省略，避免 plain string 欄位 unmarshal 失敗
      const body: Record<string, unknown> = {};
      for (const [k, v] of Object.entries(patch)) {
        if (v === undefined) continue;
        if (k === "avatar_url" && (v === null || v === "")) {
          body.avatar_url = "";
          continue;
        }
        if (v === null) continue;
        body[k] = v;
      }
      const raw = await apiRequest<Record<string, unknown>>("/api/v1/auth/profile", {
        method: "PATCH",
        body,
      });
      return normalizeMember(raw);
    },
    async requestPasswordReset(email) {
      let language = "zh-TW";
      try {
        const raw = localStorage.getItem("harbor.ui.prefs");
        if (raw) {
          const p = JSON.parse(raw) as { locale?: string };
          if (p.locale === "en") language = "en";
        }
      } catch {
        /* ignore */
      }
      const data = await apiRequest<{
        ok: true;
        message: string;
      }>("/api/v1/auth/password/request-reset", {
        method: "POST",
        body: { email, language },
        auth: false,
      });
      return {
        ok: true as const,
        message: data.message,
      };
    },
    async resetPassword(token, newPassword, emailArg) {
      const params = new URLSearchParams(window.location.search);
      const email = (emailArg || params.get("email") || "").trim();
      const data = await apiRequest<{ ok?: boolean; message?: string }>(
        "/api/v1/auth/password/reset",
        {
          method: "POST",
          body: { email, token, code: token, new_password: newPassword },
          auth: false,
        },
      );
      return {
        ok: true as const,
        message: data?.message || "password updated",
      };
    },
    async sendEmailVerificationCode() {
      const data = await apiRequest<{ ok: true; message: string }>(
        "/api/v1/auth/email/send-code",
        { method: "POST", body: {} },
      );
      return { ok: true as const, message: data.message };
    },
    async verifyEmail(code) {
      const raw = await apiRequest<Record<string, unknown>>("/api/v1/auth/email/verify", {
        method: "POST",
        body: { code },
      });
      return normalizeMember(raw);
    },
    async uploadImage(content, kind = "avatar") {
      const data = await apiRequest<{ url: string; key?: string }>("/api/v1/media/upload", {
        method: "POST",
        body: { content, kind },
      });
      return { url: data.url, key: data.key };
    },
  };
}

function createLiveAdmin(): AdminUsersRepo {
  return {
    async listUsers() {
      const page = await this.listUsersPage({ page: 1, pageSize: 50 });
      return page.list;
    },
    async listUsersPage(opts) {
      const page = opts?.page ?? 1;
      const pageSize = opts?.pageSize ?? 20;
      const query = opts?.query ?? "";
      const q = new URLSearchParams({
        page: String(page),
        pageSize: String(pageSize),
        query,
      });
      const data = await apiRequest<{
        list: Record<string, unknown>[];
        pagination: {
          page: number;
          pageSize: number;
          total: number;
          totalPages: number;
        };
      }>(`/api/v1/admin/members?${q}`);
      return {
        list: (data.list ?? []).map(normalizeAdmin),
        page: data.pagination?.page ?? page,
        pageSize: data.pagination?.pageSize ?? pageSize,
        total: data.pagination?.total ?? 0,
        totalPages: data.pagination?.totalPages ?? 1,
      };
    },
    async getUser(uid) {
      try {
        const raw = await apiRequest<Record<string, unknown>>(`/api/v1/admin/members/${uid}`);
        return normalizeAdmin(raw);
      } catch {
        return null;
      }
    },
    async createMember(input: AdminCreateMemberInput) {
      const data = await apiRequest<{
        user: Record<string, unknown>;
        temporary_password: string;
      }>("/api/v1/admin/members", {
        method: "POST",
        body: {
          display_name: input.display_name,
          email: input.email,
          password: input.password,
          roles: input.roles,
          email_verified: input.email_verified,
          bio: input.bio,
        },
      });
      return {
        user: normalizeAdmin(data.user),
        temporary_password: data.temporary_password,
      };
    },
    async setSuspended(uid, suspended) {
      const raw = await apiRequest<Record<string, unknown>>(
        `/api/v1/admin/members/${uid}/suspend`,
        { method: "POST", body: { suspended } },
      );
      return normalizeAdmin(raw);
    },
    async setEmailVerified(uid, verified) {
      const raw = await apiRequest<Record<string, unknown>>(
        `/api/v1/admin/members/${uid}/email-verified`,
        { method: "POST", body: { verified } },
      );
      return normalizeAdmin(raw);
    },
    async setRoles(uid, roles) {
      const raw = await apiRequest<Record<string, unknown>>(
        `/api/v1/admin/members/${uid}/roles`,
        { method: "POST", body: { roles } },
      );
      return normalizeAdmin(raw);
    },
    async resetPassword(uid, newPassword) {
      const data = await apiRequest<{
        user: Record<string, unknown>;
        temporary_password: string;
      }>(`/api/v1/admin/members/${uid}/reset-password`, {
        method: "POST",
        body: { new_password: newPassword },
      });
      return {
        user: normalizeAdmin(data.user),
        temporary_password: data.temporary_password,
      };
    },
  };
}

function mapAiSettings(raw: Record<string, unknown>): AiSettings {
  const models = Array.isArray(raw.models) ? (raw.models as string[]).map(String) : [];
  const selected =
    raw.selected_model != null && String(raw.selected_model) !== ""
      ? String(raw.selected_model)
      : String(raw.model ?? "");
  return {
    provider: String(raw.provider ?? "xai"),
    model: String(raw.model ?? selected),
    research_provider: String(raw.research_provider || raw.provider || "xai"),
    research_model: String(raw.research_model || raw.model || selected),
    api_key_configured: Boolean(raw.api_key_configured),
    research_api_key_configured: Boolean(
      raw.research_api_key_configured ?? raw.api_key_configured,
    ),
    selected_model: selected,
    models_provider: String(raw.models_provider || raw.provider || "xai"),
    models,
    models_from_cache: Boolean(raw.models_from_cache),
    models_error: raw.models_error != null ? String(raw.models_error) : undefined,
    platform_key_available: Boolean(raw.platform_key_available),
  };
}

function createLiveSettings(): SettingsRepo {
  return {
    getDataSource: () => "live" as const,
    setDataSource: () => {
      /* mock 資料源已移除，忽略 */
    },
    async getAi(provider) {
      const q =
        provider && provider.trim()
          ? `?provider=${encodeURIComponent(provider.trim())}`
          : "";
      const raw = await apiRequest<Record<string, unknown>>(`/api/v1/settings/ai${q}`);
      return mapAiSettings(raw);
    },
    async saveAi(patch) {
      const raw = await apiRequest<Record<string, unknown>>("/api/v1/settings/ai", {
        method: "PUT",
        body: patch,
      });
      return mapAiSettings(raw);
    },
    async getPlacement() {
      const raw = await apiRequest<PlacementSettings>("/api/v1/settings/placement");
      return {
        web_search_provider: "exa",
        expand_strategy: raw.expand_strategy === "brave" ? "hybrid" : raw.expand_strategy,
        brave_api_key_configured: false,
        exa_api_key_configured: Boolean(raw.exa_api_key_configured),
        dev_mode_enabled: Boolean(raw.dev_mode_enabled),
      };
    },
    async savePlacement(patch) {
      await apiRequest("/api/v1/settings/placement", { method: "PUT", body: patch });
      return this.getPlacement();
    },
    async listModels(provider) {
      // 仍支援舊呼叫；新 UI 請用 getAi(provider) 一次拿 models + selected_model
      const data = await apiRequest<{
        list: string[];
        selected_model?: string;
      }>(`/api/v1/settings/models?provider=${encodeURIComponent(provider)}`);
      return data.list ?? [];
    },
    async getThreadsStatus(): Promise<ThreadsPlatformStatus> {
      const raw = await apiRequest<Record<string, unknown>>("/api/v1/settings/threads");
      return {
        provider: String(raw.provider ?? "fake"),
        configured: Boolean(raw.configured),
        oauth_ready: Boolean(raw.oauth_ready ?? true),
        connect_path: String(raw.connect_path ?? "/app/crew"),
        callback_url: String(raw.callback_url ?? ""),
        public_web_base: String(raw.public_web_base ?? ""),
        app_id_masked: raw.app_id_masked != null ? String(raw.app_id_masked) : undefined,
        hint: String(raw.hint ?? ""),
      };
    },
  };
}

function mapUnixNano(v: unknown): number | null {
  if (v === null || v === undefined || v === "") return null;
  // 字串保留大整數；數字依量級判斷 sec/ms/ns
  if (typeof v === "string") {
    const s = v.trim();
    if (!s) return null;
    try {
      const bi = BigInt(s);
      if (bi <= 0n) return null;
      if (bi < 1_000_000_000_000n) return Number(bi) * 1e9;
      if (bi < 1_000_000_000_000_000n) return Number(bi) * 1e6;
      return Number(bi / 1_000_000n) * 1e6;
    } catch {
      /* fall through */
    }
  }
  const n = Number(v);
  if (!Number.isFinite(n) || n <= 0) return null;
  if (n < 1e12) return Math.round(n * 1e9);
  if (n < 1e15) return Math.round(n * 1e6);
  return n;
}

function mapAccount(raw: Record<string, unknown>): ThreadsAccount {
  return {
    id: String(raw.id ?? ""),
    username: String(raw.username ?? ""),
    display_name: String(raw.display_name ?? ""),
    connection: (raw.connection as ThreadsAccount["connection"]) ?? "disconnected",
    is_usable: Boolean(raw.is_usable),
    // 留空字串讓 AccountAvatar 退回 var(--hb-brand)，寫死色碼會不跟著深淺模式切。
    avatar_color: String(raw.avatar_color ?? ""),
    avatar_url: raw.avatar_url != null ? String(raw.avatar_url) : null,
    error_message: raw.error_message != null ? String(raw.error_message) : undefined,
    session_expires_at: mapUnixNano(raw.session_expires_at),
    session_refreshed_at: mapUnixNano(raw.session_refreshed_at),
  };
}

function mapJob(raw: Record<string, unknown>): Job {
  return {
    id: String(raw.id ?? ""),
    template_type: String(raw.template_type ?? ""),
    status: (raw.status as Job["status"]) ?? "pending",
    progress_summary: String(raw.progress_summary ?? ""),
    progress_percent: Number(raw.progress_percent ?? 0),
    error: raw.error != null ? String(raw.error) : undefined,
    ref_id: raw.ref_id != null && String(raw.ref_id) !== "" ? String(raw.ref_id) : undefined,
    payload: raw.payload != null ? String(raw.payload) : undefined,
    run_after: raw.run_after != null && Number(raw.run_after) > 0 ? Number(raw.run_after) : undefined,
    created_at: Number(raw.created_at ?? 0),
    updated_at: Number(raw.updated_at ?? 0),
    completed_at: raw.completed_at != null ? Number(raw.completed_at) : undefined,
  };
}

function mapNotif(raw: Record<string, unknown>): AppNotification {
  return {
    id: String(raw.id ?? ""),
    title: String(raw.title ?? ""),
    body: String(raw.body ?? ""),
    kind: (raw.kind as AppNotification["kind"]) ?? "system",
    ref_type: (raw.ref_type as AppNotification["ref_type"]) ?? "none",
    ref_id: raw.ref_id != null ? String(raw.ref_id) : undefined,
    read_at: raw.read_at != null && Number(raw.read_at) !== 0 ? Number(raw.read_at) : null,
    created_at: Number(raw.created_at ?? 0),
  };
}

function createLiveAccounts(): AccountsRepo {
  return {
    async list() {
      const data = await apiRequest<{ list: Record<string, unknown>[] }>(
        "/api/v1/threads-accounts",
      );
      return (data.list ?? []).map(mapAccount);
    },
    async createMock() {
      throw new Error("live 模式請使用 OAuth 連帳（createMock 僅 mock）");
    },
    async oauthStart() {
      const data = await apiRequest<{ authorize_url: string; state: string }>(
        "/api/v1/threads-accounts/oauth/start",
      );
      return { authorize_url: data.authorize_url, state: data.state };
    },
    async remove(id) {
      await apiRequest(`/api/v1/threads-accounts/${encodeURIComponent(id)}`, {
        method: "DELETE",
      });
    },
    async refreshSession(id) {
      const raw = await apiRequest<Record<string, unknown>>(
        `/api/v1/threads-accounts/${encodeURIComponent(id)}/refresh`,
        { method: "POST", body: {} },
      );
      return mapAccount(raw);
    },
  };
}

function createLiveJobs(): JobsRepo {
  return {
    async list(query) {
      const tab = query?.tab ?? "active";
      const page = query?.page ?? 1;
      const pageSize = query?.pageSize ?? (query ? 10 : 50);
      const qs = new URLSearchParams({
        tab,
        page: String(page),
        pageSize: String(pageSize),
      });
      const data = await apiRequest<{
        list?: Record<string, unknown>[];
        pagination?: {
          page?: number;
          pageSize?: number;
          total?: number;
          totalPages?: number;
        };
      }>(`/api/v1/jobs?${qs.toString()}`);
      const list = (data.list ?? []).map(mapJob);
      const p = data.pagination ?? {};
      const total = Number(p.total ?? list.length);
      const ps = Number(p.pageSize ?? pageSize) || pageSize;
      const pg = Number(p.page ?? page) || page;
      const totalPages =
        Number(p.totalPages) ||
        (ps > 0 ? Math.max(1, Math.ceil(total / ps)) : 1);
      return {
        list,
        pagination: {
          page: pg,
          pageSize: ps,
          total,
          totalPages,
        },
      };
    },
    async get(id) {
      try {
        const raw = await apiRequest<Record<string, unknown>>(
          `/api/v1/jobs/${encodeURIComponent(id)}`,
        );
        return mapJob(raw);
      } catch {
        return null;
      }
    },
    async startDemo() {
      const raw = await apiRequest<Record<string, unknown>>("/api/v1/jobs/demo", {
        method: "POST",
        body: {},
      });
      return mapJob(raw);
    },
    async remove(id) {
      await apiRequest<{ ok: boolean }>(`/api/v1/jobs/${encodeURIComponent(id)}`, {
        method: "DELETE",
      });
    },
  };
}

function createLiveNotifications(): NotificationsRepo {
  return {
    async list() {
      const data = await apiRequest<{ list: Record<string, unknown>[] }>(
        "/api/v1/notifications",
      );
      return (data.list ?? []).map(mapNotif);
    },
    async unreadCount() {
      const data = await apiRequest<{ count: number }>("/api/v1/notifications/unread-count");
      return Number(data.count ?? 0);
    },
    async markRead(id) {
      await apiRequest(`/api/v1/notifications/${encodeURIComponent(id)}/read`, {
        method: "POST",
        body: {},
      });
    },
    async markAllRead() {
      await apiRequest("/api/v1/notifications/read-all", { method: "POST", body: {} });
    },
  };
}

function mapMeterBlock(
  plan: (typeof PLANS)[PlanId],
  platformBy: Record<string, unknown> | undefined,
): UsageMonthSummary["by_meter"] {
  const by_meter = {} as UsageMonthSummary["by_meter"];
  for (const m of Object.keys(plan.soft_caps) as UsageMeter[]) {
    const p = (platformBy?.[m] as Record<string, unknown> | undefined) ?? {};
    // 上限對照只看 platform；byok 不進分項鎖／進度（只出現在 ledger）
    const credits = Number(p.credits ?? 0);
    const platformCount = Number(p.count ?? 0);
    const soft = plan.soft_caps[m] || 1; // 分項點數上限（非次數）
    const pct = soft > 0 ? Math.min(999, Math.round((credits / soft) * 100)) : 0;
    by_meter[m] = {
      credits,
      count: platformCount,
      soft_cap: soft,
      pct,
    };
  }
  return by_meter;
}

export function mapByokMeterBlock(
  plan: (typeof PLANS)[PlanId],
  raw: Record<string, unknown> | undefined,
): UsageMonthSummary["byok"]["by_meter"] {
  const by_meter = {} as UsageMonthSummary["byok"]["by_meter"];
  for (const m of Object.keys(plan.soft_caps) as UsageMeter[]) {
    const row = (raw?.[m] as Record<string, unknown> | undefined) ?? {};
    by_meter[m] = {
      credits: Number(row.credits ?? 0),
      count: Number(row.count ?? 0),
    };
  }
  return by_meter;
}

function mapAnalyticsMeterBlock(
  raw: Record<string, unknown> | undefined,
): Record<UsageMeter, { credits: number; count: number }> {
  const mapped = {} as Record<UsageMeter, { credits: number; count: number }>;
  for (const meter of Object.keys(PLANS.free.soft_caps) as UsageMeter[]) {
    const row = (raw?.[meter] as Record<string, unknown> | undefined) ?? {};
    mapped[meter] = {
      credits: Number(row.credits ?? 0),
      count: Number(row.count ?? 0),
    };
  }
  return mapped;
}

export function mapTenantAnalytics(raw: Record<string, unknown>): TenantUsageAnalytics {
  const byok = (raw.byok as Record<string, unknown> | undefined) ?? {};
  return {
    granularity: String(raw.granularity ?? "month") as UsageGranularity,
    from: String(raw.from ?? ""),
    to: String(raw.to ?? ""),
    range_label: String(raw.range_label ?? ""),
    purchased_credits: Number(raw.purchased_credits ?? 0),
    consumed_credits: Number(raw.consumed_credits ?? 0),
    remaining_credits: Number(raw.remaining_credits ?? 0),
    pct: Number(raw.pct ?? 0),
    ai_calls: Number(raw.ai_calls ?? 0),
    search_calls: Number(raw.search_calls ?? 0),
    by_meter: mapAnalyticsMeterBlock(raw.by_meter as Record<string, unknown> | undefined),
    byok: {
      call_count: Number(byok.call_count ?? 0),
      by_meter: mapAnalyticsMeterBlock(byok.by_meter as Record<string, unknown> | undefined),
    },
    series: ((raw.series as Record<string, unknown>[] | undefined) ?? []).map((row) => ({
      key: String(row.key ?? ""),
      label: String(row.label ?? row.key ?? ""),
      purchased_credits: Number(row.purchased_credits ?? 0),
      consumed_credits: Number(row.consumed_credits ?? 0),
      ai_calls: Number(row.ai_calls ?? 0),
      search_calls: Number(row.search_calls ?? 0),
      byok_call_count: Number(row.byok_call_count ?? 0),
    })),
    members: ((raw.members as Record<string, unknown>[] | undefined) ?? []).map((row) => {
      const planIdRaw = String(row.plan_id ?? "free").toLowerCase();
      const planId = (planIdRaw in PLANS ? planIdRaw : "free") as PlanId;
      return {
        uid: String(row.uid ?? ""),
        email: String(row.email ?? ""),
        display_name: String(row.display_name ?? row.email ?? row.uid ?? ""),
        unlimited: Boolean(row.unlimited),
        plan_id: planId,
        purchased_credits: Number(row.purchased_credits ?? 0),
        total_credits: Number(row.total_credits ?? 0),
        ai_calls: Number(row.ai_calls ?? 0),
        search_calls: Number(row.search_calls ?? 0),
        remaining_credits: Number(row.remaining_credits ?? 0),
        pct: Number(row.pct ?? 0),
      };
    }),
  };
}

function mapUsageEvent(raw: Record<string, unknown>): UsageEvent {
  return {
    id: String(raw.id ?? ""),
    uid: String(raw.uid ?? ""),
    meter: String(raw.meter ?? "ai_copy") as UsageMeter,
    credits: Number(raw.credits ?? 0),
    key_mode: (String(raw.key_mode ?? "platform") as UsageKeyMode) || "platform",
    label: String(raw.label ?? ""),
    source: String(raw.source ?? ""),
    created_at: Number(raw.created_at ?? 0),
  };
}

function createLiveUsage(): UsageRepo {
  return {
    async getSummary(monthKey) {
      const q = monthKey ? `?month_key=${encodeURIComponent(monthKey)}` : "";
      const raw = await apiRequest<Record<string, unknown>>(`/api/v1/usage/summary${q}`);
      // plan_id 正規化；配給一律以 FE PLANS 為準（與後端 domain.Plans 同步數字）
      // 不可盲信後端舊 binary 的 credits_total（買 Starter 卻顯示 Free 上限的根因之一）
      const planIdRaw = String(raw.plan_id ?? "free").toLowerCase().trim();
      const planId = (planIdRaw in PLANS ? planIdRaw : "free") as PlanId;
      const plan = PLANS[planId];
      const platform = (raw.platform as Record<string, unknown>) ?? {};
      const byok = (raw.byok as Record<string, unknown>) ?? {};
      const cap = plan.monthly_credits;
      const used = Number(platform.credits_used ?? 0);
      const remaining = raw.unlimited
        ? Math.max(0, Number(platform.credits_remaining ?? Math.max(0, cap - used)))
        : Math.max(0, cap - used);
      const pct = cap > 0 ? Math.min(999, Math.round((used / cap) * 100)) : 0;

      const platBy = (platform.by_meter as Record<string, unknown> | undefined) ?? {};
      const byokBy = (byok.by_meter as Record<string, unknown> | undefined) ?? {};
      const by_meter = mapMeterBlock(plan, platBy);
      const byok_by_meter = mapByokMeterBlock(plan, byokBy);

      // 明細列表：與摘要一併載入（用量頁 ledger / 細項）
      let events: UsageEvent[] = [];
      try {
        const evQ = monthKey
          ? `?month_key=${encodeURIComponent(monthKey)}&limit=200`
          : "?limit=200";
        const data = await apiRequest<{ list: Record<string, unknown>[] }>(
          `/api/v1/usage/events${evQ}`,
        );
        events = (data.list ?? []).map(mapUsageEvent);
      } catch {
        events = [];
      }

      // 狀態列 AI／Search：顯示「點數」才與方案配給同一單位（不是混加次數）
      const ai_calls =
        (by_meter.ai_copy?.credits ?? 0) +
        (by_meter.ai_research?.credits ?? 0) +
        (by_meter.ai_image?.credits ?? 0);
      const search_calls = by_meter.web_search?.credits ?? 0;

      return {
        month_key: String(raw.month_key ?? ""),
        uid: String(raw.uid ?? ""),
        plan,
        unlimited: Boolean(raw.unlimited),
        // FE 慣例：total_credits = 本月已用（platform 點數）；配給看 plan.monthly_credits
        total_credits: used,
        remaining_credits: remaining,
        pct,
        ai_calls,
        search_calls,
        by_meter,
        events,
        platform: {
          credits_used: used,
          credits_remaining: remaining,
          credits_total: cap,
          call_count: Number(platform.call_count ?? 0),
        },
        byok: {
          call_count: Number(byok.call_count ?? 0),
          by_meter: byok_by_meter,
        },
      };
    },
    async listEvents(limit = 100) {
      const data = await apiRequest<{ list: Record<string, unknown>[] }>(
        `/api/v1/usage/events?limit=${limit}`,
      );
      return (data.list ?? []).map(mapUsageEvent);
    },
    async getPlanId() {
      const p = await this.getMemberPrefs();
      return p.plan_id;
    },
    async getMemberPrefs(uid) {
      const q = uid ? `?uid=${encodeURIComponent(uid)}` : "";
      const raw = await apiRequest<Record<string, unknown>>(`/api/v1/usage/prefs${q}`);
      const id = String(raw.plan_id ?? "free").toLowerCase().trim();
      return {
        plan_id: (id in PLANS ? id : "free") as PlanId,
        unlimited: Boolean(raw.unlimited),
      };
    },
    async setMemberPrefs(uid, patch) {
      const body: Record<string, unknown> = { uid };
      if (patch.plan_id != null) body.plan_id = patch.plan_id;
      if (patch.unlimited != null) body.unlimited = patch.unlimited;
      const raw = await apiRequest<Record<string, unknown>>("/api/v1/usage/prefs", {
        method: "POST",
        body,
      });
      return {
        plan_id: String(raw.plan_id ?? "free") as PlanId,
        unlimited: Boolean(raw.unlimited),
      } as UsageMemberPrefs;
    },
    async getTenantSummary(monthKey) {
      const q = monthKey ? `?month_key=${encodeURIComponent(monthKey)}` : "";
      const raw = await apiRequest<Record<string, unknown>>(`/api/v1/usage/tenant-summary${q}`);
      // 補顯示名稱（admin members）
      const nameByUid = new Map<string, { email: string; display_name: string }>();
      try {
        const admin = await apiRequest<{
          list?: Record<string, unknown>[];
        }>("/api/v1/admin/members?page=1&pageSize=50");
        for (const row of admin.list ?? []) {
          const uid = String(row.uid ?? "");
          if (!uid) continue;
          nameByUid.set(uid, {
            email: String(row.email ?? ""),
            display_name: String(row.display_name ?? row.email ?? uid),
          });
        }
      } catch {
        /* non-admin or fail */
      }

      const members = ((raw.members as Record<string, unknown>[]) ?? []).map((m) => {
        const uid = String(m.uid ?? "");
        const planId = String(m.plan_id ?? "free") as PlanId;
        const plan = PLANS[planId] ?? PLANS.free;
        const used = Number(m.platform_credits_used ?? 0);
        const purchased = plan.monthly_credits;
        const remaining = Math.max(0, purchased - used);
        const pct = purchased > 0 ? Math.min(999, Math.round((used / purchased) * 100)) : 0;
        const names = nameByUid.get(uid);
        return {
          uid,
          email: names?.email ?? "",
          display_name: names?.display_name || `uid ${uid}`,
          unlimited: Boolean(m.unlimited),
          plan_id: planId,
          purchased_credits: purchased,
          total_credits: used,
          ai_calls: 0,
          search_calls: Number(m.byok_call_count ?? 0),
          remaining_credits: remaining,
          pct,
        };
      });
      members.sort((a, b) => b.total_credits - a.total_credits);

      const consumed = Number(raw.platform_credits_used ?? 0);
      const purchasedSum = members.reduce((s, m) => s + m.purchased_credits, 0);
      return {
        month_key: String(raw.month_key ?? ""),
        total_credits: consumed,
        purchased_credits: purchasedSum,
        consumed_credits: consumed,
        remaining_credits: Math.max(0, purchasedSum - consumed),
        ai_calls: 0,
        search_calls: Number(raw.byok_call_count ?? 0),
        by_meter: {
          ai_copy: { credits: 0, count: 0 },
          ai_research: { credits: 0, count: 0 },
          web_search: { credits: 0, count: 0 },
          ai_image: { credits: 0, count: 0 },
        },
        members,
      };
    },
    async getTenantAnalytics(query) {
		const params = new URLSearchParams();
		params.set("granularity", query?.granularity ?? "month");
		if (query?.from) params.set("from", query.from);
		if (query?.to) params.set("to", query.to);
		const raw = await apiRequest<Record<string, unknown>>(
			`/api/v1/usage/tenant-analytics?${params.toString()}`,
		);
		return mapTenantAnalytics(raw);
    },
    async createCheckoutSession(plan_id, request_id) {
      const raw = await apiRequest<Record<string, unknown>>(
        "/api/v1/billing/checkout-sessions",
        { method: "POST", body: { plan_id, request_id } },
      );
      return mapBillingCheckoutSession(raw);
    },
    async getCheckoutSession(id) {
      const raw = await apiRequest<Record<string, unknown>>(
        `/api/v1/billing/checkout-sessions/${encodeURIComponent(id)}`,
      );
      return mapBillingCheckoutStatus(raw);
    },
    async getBillingSubscription() {
      const raw = await apiRequest<Record<string, unknown>>("/api/v1/billing/subscription");
      return mapBillingSubscription(raw);
    },
    async createBillingPortalSession() {
      const raw = await apiRequest<Record<string, unknown>>("/api/v1/billing/portal-sessions", {
        method: "POST",
      });
      return { url: String(raw.url ?? "") };
    },
    async listMyPurchases(limit = 20) {
      const data = await apiRequest<{ list: Record<string, unknown>[] }>(
        "/api/v1/usage/purchases",
      );
      return (data.list ?? []).slice(0, limit).map(
        (raw): PlanPurchase => ({
          id: String(raw.id ?? ""),
          uid: "",
          plan_id: String(raw.plan_id ?? "free") as PlanId,
          amount_twd: Number(raw.amount_twd ?? 0),
          status: "paid",
          mock_ref: raw.mock_ref != null ? String(raw.mock_ref) : undefined,
          created_at: Number(raw.created_at ?? 0),
        }),
      );
    },
  };
}

// --- M4 Studio live mappers ---

function mapStyleDimensions(
  raw: unknown,
): Persona["style"]["dimensions"] {
  if (!raw || typeof raw !== "object") return {};
  const out: Persona["style"]["dimensions"] = {};
  for (const [key, val] of Object.entries(raw as Record<string, unknown>)) {
    if (!val || typeof val !== "object") continue;
    const d = val as Record<string, unknown>;
    const summary = String(d.summary ?? "").trim();
    if (!summary) continue;
    out[key as keyof Persona["style"]["dimensions"]] = {
      summary,
      evidence: Array.isArray(d.evidence)
        ? (d.evidence as unknown[]).map((x) => String(x))
        : [],
    };
  }
  return out;
}

function mapPersona(raw: Record<string, unknown>): Persona {
  const style = (raw.style as Record<string, unknown>) ?? {};
  const draft = (style.draft as Record<string, unknown>) ?? {};
  const guard = (raw.guard as Record<string, unknown>) ?? {};
  return {
    id: String(raw.id ?? ""),
    name: String(raw.name ?? ""),
    brief: String(raw.brief ?? ""),
    status: (raw.status as Persona["status"]) ?? "empty",
    style: {
      dimensions: mapStyleDimensions(style.dimensions),
      draft: {
        identity: String(draft.identity ?? ""),
        tone: String(draft.tone ?? ""),
        audience: String(draft.audience ?? ""),
        hooks: String(draft.hooks ?? ""),
        languageFingerprint: String(draft.language_fingerprint ?? ""),
        rhythm: String(draft.rhythm ?? ""),
        punctuation: String(draft.punctuation ?? ""),
        contentPatterns: String(draft.content_patterns ?? ""),
        knowledgeTranslation: String(draft.knowledge_translation ?? ""),
        ctaStyle: String(draft.cta_style ?? ""),
        examples: String(draft.examples ?? ""),
        avoid: String(draft.avoid ?? ""),
      },
      draftText: String(style.draft_text ?? ""),
      source: (style.source as Persona["style"]["source"]) || "manual",
      benchmarkUsername:
        style.benchmark_username != null ? String(style.benchmark_username) : undefined,
      sourceLabel: style.source_label != null ? String(style.source_label) : undefined,
      sampleCount: Number(style.sample_count ?? 0),
      analyzedAt: style.analyzed_at != null ? Number(style.analyzed_at) : undefined,
      samplePreviews: Array.isArray(style.sample_previews)
        ? (style.sample_previews as string[])
        : undefined,
    },
    guard: {
      avoid: Array.isArray(guard.avoid) ? (guard.avoid as string[]) : [],
      maxChars: Number(guard.max_chars ?? 500),
      banAiTone: Boolean(guard.ban_ai_tone),
    },
    voice: raw.voice != null ? String(raw.voice) : undefined,
    notes: raw.notes != null ? String(raw.notes) : undefined,
    learning_summary: raw.learning_summary != null ? String(raw.learning_summary) : undefined,
    learning_version: raw.learning_version != null ? Number(raw.learning_version) : undefined,
    learned_from_posts_count:
      raw.learned_from_posts_count != null ? Number(raw.learned_from_posts_count) : undefined,
    last_learned_at: raw.last_learned_at != null ? Number(raw.last_learned_at) : undefined,
  };
}

function personaToBody(p: Persona): Record<string, unknown> {
  const dimensions: Record<string, { summary: string; evidence?: string[] }> = {};
  for (const [k, v] of Object.entries(p.style.dimensions || {})) {
    if (!v?.summary) continue;
    dimensions[k] = { summary: v.summary, evidence: v.evidence };
  }
  return {
    id: p.id,
    name: p.name,
    brief: p.brief,
    status: p.status,
    style: {
      draft: {
        identity: p.style.draft.identity,
        tone: p.style.draft.tone,
        audience: p.style.draft.audience,
        hooks: p.style.draft.hooks,
        language_fingerprint: p.style.draft.languageFingerprint,
        rhythm: p.style.draft.rhythm,
        punctuation: p.style.draft.punctuation,
        content_patterns: p.style.draft.contentPatterns,
        knowledge_translation: p.style.draft.knowledgeTranslation,
        cta_style: p.style.draft.ctaStyle,
        examples: p.style.draft.examples,
        avoid: p.style.draft.avoid,
      },
      draft_text: p.style.draftText,
      source: p.style.source,
      benchmark_username: p.style.benchmarkUsername,
      source_label: p.style.sourceLabel,
      sample_count: p.style.sampleCount,
      analyzed_at: p.style.analyzedAt,
      sample_previews: p.style.samplePreviews,
      dimensions,
    },
    guard: {
      avoid: p.guard.avoid,
      max_chars: p.guard.maxChars,
      ban_ai_tone: p.guard.banAiTone,
    },
    voice: p.voice,
    notes: p.notes,
  };
}

function mapExternal(raw: Record<string, unknown> | null | undefined): ExternalThreadTarget | null {
  if (!raw || !raw.url) return null;
  return {
    url: String(raw.url ?? ""),
    raw_url: raw.raw_url != null ? String(raw.raw_url) : undefined,
    shortcode: raw.shortcode != null ? String(raw.shortcode) : undefined,
    author_username: raw.author_username != null ? String(raw.author_username) : undefined,
    text_preview: raw.text_preview != null ? String(raw.text_preview) : undefined,
    media_id: raw.media_id != null ? String(raw.media_id) : undefined,
    resolved_at: raw.resolved_at != null ? Number(raw.resolved_at) : undefined,
  };
}

function mapPlay(raw: Record<string, unknown>): ThreadPlay {
  const stepsRaw = Array.isArray(raw.steps) ? raw.steps : [];
  return {
    id: String(raw.id ?? ""),
    title: String(raw.title ?? ""),
    topic: String(raw.topic ?? ""),
    status: (raw.status as ThreadPlay["status"]) ?? "draft",
    lead_account_id: String(raw.lead_account_id ?? ""),
    cast_account_ids: Array.isArray(raw.cast_account_ids)
      ? (raw.cast_account_ids as string[])
      : [],
    target_own_post_id:
      raw.target_own_post_id != null && String(raw.target_own_post_id) !== ""
        ? String(raw.target_own_post_id)
        : null,
    target_external: mapExternal(raw.target_external as Record<string, unknown>),
    steps: stepsRaw.map((s) => {
      const step = s as Record<string, unknown>;
      return {
        id: String(step.id ?? ""),
        sort_order: Number(step.sort_order ?? 0),
        kind: (step.kind as "root" | "reply") ?? "reply",
        account_id: String(step.account_id ?? ""),
        text: String(step.text ?? ""),
        delay_from_previous_sec: Number(step.delay_from_previous_sec ?? 0),
        persona_id: step.persona_id != null ? String(step.persona_id) : null,
        brand_id: step.brand_id != null ? String(step.brand_id) : null,
        image_urls: Array.isArray(step.image_urls) ? (step.image_urls as string[]) : undefined,
      };
    }),
    schedule_start_at: Number(raw.schedule_start_at ?? 0),
    interval_minutes: raw.interval_minutes != null ? Number(raw.interval_minutes) : undefined,
    created_at: Number(raw.created_at ?? 0),
    updated_at: Number(raw.updated_at ?? 0),
  };
}

function playToBody(play: ThreadPlay): Record<string, unknown> {
  return {
    id: play.id,
    title: play.title,
    topic: play.topic,
    status: play.status,
    lead_account_id: play.lead_account_id,
    cast_account_ids: play.cast_account_ids,
    target_own_post_id: play.target_own_post_id || undefined,
    target_external: play.target_external?.url
      ? {
          url: play.target_external.url,
          raw_url: play.target_external.raw_url,
          shortcode: play.target_external.shortcode,
          author_username: play.target_external.author_username,
          text_preview: play.target_external.text_preview,
          media_id: play.target_external.media_id,
          resolved_at: play.target_external.resolved_at,
        }
      : undefined,
    steps: play.steps.map((s) => ({
      id: s.id,
      sort_order: s.sort_order,
      kind: s.kind,
      account_id: s.account_id,
      text: s.text,
      delay_from_previous_sec: s.delay_from_previous_sec,
      persona_id: s.persona_id || undefined,
      brand_id: s.brand_id || undefined,
      image_urls: s.image_urls,
    })),
    schedule_start_at: play.schedule_start_at,
    interval_minutes: play.interval_minutes,
  };
}

function mapOutbox(raw: Record<string, unknown>): OutboxBundle {
  const stepsRaw = Array.isArray(raw.steps) ? raw.steps : [];
  return {
    id: String(raw.id ?? ""),
    play_id: String(raw.play_id ?? ""),
    title: String(raw.title ?? ""),
    status: (raw.status as OutboxBundle["status"]) ?? "scheduling",
    steps: stepsRaw.map((s) => {
      const step = s as Record<string, unknown>;
      return {
        id: String(step.id ?? ""),
        step_id: String(step.step_id ?? ""),
        sort_order: Number(step.sort_order ?? 0),
        kind: (step.kind as "root" | "reply") ?? "root",
        account_id: String(step.account_id ?? ""),
        text: String(step.text ?? ""),
        status: (step.status as OutboxBundle["steps"][0]["status"]) ?? "scheduled",
        error: step.error != null ? String(step.error) : undefined,
        scheduled_at: step.scheduled_at != null ? Number(step.scheduled_at) : undefined,
        published_at: step.published_at != null ? Number(step.published_at) : undefined,
      };
    }),
    created_at: Number(raw.created_at ?? 0),
    updated_at: Number(raw.updated_at ?? 0),
  };
}

function mapOwnPost(raw: Record<string, unknown>): OwnPost {
  const repliesRaw = Array.isArray(raw.replies) ? raw.replies : [];
  return {
    id: String(raw.id ?? ""),
    account_id: String(raw.account_id ?? ""),
    media_id: raw.media_id != null ? String(raw.media_id) : undefined,
    text: String(raw.text ?? ""),
    media_type: String(raw.media_type ?? "TEXT_POST"),
    media_url: raw.media_url != null ? String(raw.media_url) : null,
    thumbnail_url: raw.thumbnail_url != null ? String(raw.thumbnail_url) : null,
    permalink: raw.permalink != null ? String(raw.permalink) : undefined,
    shortcode: raw.shortcode != null ? String(raw.shortcode) : undefined,
    topic_tag: raw.topic_tag != null ? String(raw.topic_tag) : undefined,
    like_count: Number(raw.like_count ?? 0),
    reply_count: Number(raw.reply_count ?? 0),
    repost_count: Number(raw.repost_count ?? 0),
    quote_count: Number(raw.quote_count ?? 0),
    view_count: Number(raw.view_count ?? 0),
    share_count: Number(raw.share_count ?? 0),
    insights_status: raw.insights_status != null ? String(raw.insights_status) : undefined,
    formula_summary: raw.formula_summary != null ? String(raw.formula_summary) : undefined,
    insight: raw.insight != null ? String(raw.insight) : undefined,
    formula_detail: raw.formula_detail != null ? String(raw.formula_detail) : undefined,
    replies: repliesRaw.map((r) => {
      const row = r as Record<string, unknown>;
      return {
        id: String(row.id ?? ""),
        username: String(row.username ?? ""),
        text: String(row.text ?? ""),
        created_at: Number(row.created_at ?? 0),
        like_count: row.like_count != null ? Number(row.like_count) : undefined,
        reply_status: row.reply_status as OwnPost["replies"][0]["reply_status"],
        replied_by: row.replied_by != null ? String(row.replied_by) : undefined,
        replied_at: row.replied_at != null ? Number(row.replied_at) : undefined,
        parent_reply_id:
          row.parent_reply_id != null && String(row.parent_reply_id) !== ""
            ? String(row.parent_reply_id)
            : null,
        is_mine: Boolean(row.is_mine),
        hide_status: row.hide_status != null ? String(row.hide_status) : undefined,
      };
    }),
    published_at: Number(raw.published_at ?? 0),
    reply_control: raw.reply_control != null ? String(raw.reply_control) : undefined,
  };
}

function mapMention(raw: Record<string, unknown>): MentionItem {
  return {
    id: String(raw.id ?? ""),
    account_id: String(raw.account_id ?? ""),
    media_id: raw.media_id != null ? String(raw.media_id) : undefined,
    from_username: String(raw.from_username ?? ""),
    text: String(raw.text ?? ""),
    context_snippet: String(raw.context_snippet ?? ""),
    permalink: raw.permalink != null ? String(raw.permalink) : undefined,
    status: (raw.status as MentionItem["status"]) ?? "pending",
    draft_text: raw.draft_text != null ? String(raw.draft_text) : undefined,
    created_at: Number(raw.created_at ?? 0),
  };
}

function mapViral(raw: Record<string, unknown>): ViralAnalysis {
  return {
    hooks: String(raw.hooks ?? ""),
    structure: String(raw.structure ?? ""),
    emotion: String(raw.emotion ?? ""),
    copyable: String(raw.copyable ?? raw.structure ?? ""),
    risks: String(raw.risks ?? ""),
    summary: String(raw.summary ?? ""),
  };
}

function createLivePersonas(): PersonasRepo {
  return {
    async list() {
      const data = await apiRequest<{ list: Record<string, unknown>[] }>("/api/v1/personas");
      return (data.list ?? []).map(mapPersona);
    },
    async get(id) {
      try {
        const raw = await apiRequest<Record<string, unknown>>(
          `/api/v1/personas/${encodeURIComponent(id)}`,
        );
        return mapPersona(raw);
      } catch {
        return null;
      }
    },
    async getActiveId() {
      const data = await apiRequest<{ id: string }>("/api/v1/personas/active");
      return String(data.id ?? "");
    },
    async setActiveId(id) {
      await apiRequest("/api/v1/personas/active", { method: "POST", body: { id } });
    },
    async save(persona) {
      const raw = await apiRequest<Record<string, unknown>>("/api/v1/personas", {
        method: "POST",
        body: personaToBody(persona),
      });
      return mapPersona(raw);
    },
    async remove(id) {
      await apiRequest(`/api/v1/personas/${encodeURIComponent(id)}`, { method: "DELETE" });
    },
    async analyzeFromText(id, rawText, sourceLabel) {
      const raw = await apiRequest<Record<string, unknown>>(
        `/api/v1/personas/${encodeURIComponent(id)}/analyze-text`,
        { method: "POST", body: { raw_text: rawText, source_label: sourceLabel } },
      );
      return mapPersona(raw);
    },
    async analyzeFromAccount(id, username) {
      const raw = await apiRequest<Record<string, unknown>>(
        `/api/v1/personas/${encodeURIComponent(id)}/analyze-account`,
        { method: "POST", body: { username } },
      );
      return mapPersona(raw);
    },
  };
}

function createLivePlays(): PlaysRepo {
  return {
    async list() {
      const data = await apiRequest<{ list: Record<string, unknown>[] }>("/api/v1/plays");
      return (data.list ?? []).map(mapPlay);
    },
    async listByPost(ownPostId) {
      const q = new URLSearchParams({ own_post_id: ownPostId });
      const data = await apiRequest<{ list: Record<string, unknown>[] }>(
        `/api/v1/plays/by-post?${q}`,
      );
      return (data.list ?? []).map(mapPlay);
    },
    async listByExternalUrl(url) {
      const q = new URLSearchParams({ url });
      const data = await apiRequest<{ list: Record<string, unknown>[] }>(
        `/api/v1/plays/by-external?${q}`,
      );
      return (data.list ?? []).map(mapPlay);
    },
    async resolveExternalLink(url) {
      const raw = await apiRequest<Record<string, unknown>>("/api/v1/plays/resolve-external", {
        method: "POST",
        body: { url },
      });
      return mapExternal(raw)!;
    },
    async get(id) {
      try {
        const raw = await apiRequest<Record<string, unknown>>(
          `/api/v1/plays/${encodeURIComponent(id)}`,
        );
        return mapPlay(raw);
      } catch {
        return null;
      }
    },
    async save(play) {
      const raw = await apiRequest<Record<string, unknown>>("/api/v1/plays", {
        method: "POST",
        body: playToBody(play),
      });
      return mapPlay(raw);
    },
    async remove(id) {
      await apiRequest(`/api/v1/plays/${encodeURIComponent(id)}`, { method: "DELETE" });
    },
    async submit(id) {
      const raw = await apiRequest<Record<string, unknown>>(
        `/api/v1/plays/${encodeURIComponent(id)}/submit`,
        { method: "POST", body: {} },
      );
      return mapOutbox(raw);
    },
    async generateStep(opts) {
      const data = await apiRequest<{ text: string }>("/api/v1/plays/generate-step", {
        method: "POST",
        body: {
          persona_id: opts.personaId || undefined,
          context: opts.context,
          topic: opts.topic || undefined,
          speaker_label: opts.speakerLabel || undefined,
          is_lead: opts.isLead || false,
          mode: opts.mode || "reply",
        },
      });
      return String(data.text ?? "").trim();
    },
    async generateScript(playId) {
      const data = await apiRequest<{ job_id: string; async: boolean }>(
        `/api/v1/plays/${encodeURIComponent(playId)}/generate-script`,
        { method: "POST", body: {} },
      );
      return { jobId: String(data.job_id ?? ""), async: Boolean(data.async) };
    },
  };
}

function createLiveOutbox(): OutboxRepo {
  return {
    async list() {
      const data = await apiRequest<{ list: Record<string, unknown>[] }>("/api/v1/outbox");
      return (data.list ?? []).map(mapOutbox);
    },
    async get(id) {
      try {
        const raw = await apiRequest<Record<string, unknown>>(
          `/api/v1/outbox/${encodeURIComponent(id)}`,
        );
        return mapOutbox(raw);
      } catch {
        return null;
      }
    },
    async simulateSuccess() {
      throw new Error("live 模式請等 worker 真發（simulate 僅 mock）");
    },
    async simulateRootFail() {
      throw new Error("live 模式請等 worker 真發（simulate 僅 mock）");
    },
    async retryStep(bundleId, stepId) {
      const raw = await apiRequest<Record<string, unknown>>(
        `/api/v1/outbox/${encodeURIComponent(bundleId)}/steps/${encodeURIComponent(stepId)}/retry`,
        { method: "POST", body: {} },
      );
      return mapOutbox(raw);
    },
    async remove(id) {
      await apiRequest(`/api/v1/outbox/${encodeURIComponent(id)}`, { method: "DELETE" });
    },
  };
}

function createLiveCompose(): ComposeRepo {
  return {
    async mimic(sourceText, personaId, direction, structureNotes) {
      const notes = (structureNotes || "").trim();
      const data = await apiRequest<{ text?: string; job_id?: string; async?: boolean }>(
        "/api/v1/compose/mimic",
        {
          method: "POST",
          body: {
            source_text: sourceText,
            persona_id: personaId,
            direction: direction?.trim() || undefined,
            structure_notes: notes || undefined,
          },
        },
      );
      return {
        text: data.text ? String(data.text) : undefined,
        jobId: data.job_id ? String(data.job_id) : undefined,
        async: Boolean(data.async ?? data.job_id),
      };
    },
    async personaPreview(opts) {
      const data = await apiRequest<{
        topic?: string;
        topic_source?: string;
        post_text?: string;
        reply_text?: string;
        notes?: string;
      }>("/api/v1/compose/persona-preview", {
        method: "POST",
        body: {
          persona_id: opts.personaId,
          topic: opts.topic || undefined,
          use_news: opts.useNews !== false,
        },
      });
      return {
        topic: String(data.topic ?? ""),
        topic_source: String(data.topic_source ?? "fallback"),
        post_text: String(data.post_text ?? ""),
        reply_text: String(data.reply_text ?? ""),
        notes: data.notes != null ? String(data.notes) : undefined,
      };
    },
    async analyzeViral(text) {
      const raw = await apiRequest<Record<string, unknown>>("/api/v1/compose/analyze-viral", {
        method: "POST",
        body: { text },
      });
      return mapViral(raw);
    },
    async publishSingle(opts) {
      const raw = await apiRequest<Record<string, unknown>>("/api/v1/compose/publish-single", {
        method: "POST",
        body: {
          account_id: opts.accountId,
          text: opts.text,
          title: opts.title,
          image_urls: opts.imageUrls,
          schedule_start_at: opts.schedule_start_at,
          topic_tag: opts.topicTag || undefined,
          reply_control: opts.replyControl || undefined,
        },
      });
      return mapOutbox(raw);
    },
  };
}

function createLiveOwnPosts(): OwnPostsRepo {
  return {
    async list(accountId) {
      const q = accountId ? `?account_id=${encodeURIComponent(accountId)}` : "";
      const data = await apiRequest<{ list: Record<string, unknown>[] }>(
        `/api/v1/own-posts${q}`,
      );
      return (data.list ?? []).map(mapOwnPost);
    },
    async lastSyncedAt() {
      const data = await apiRequest<{ last_synced_at: number }>(
        "/api/v1/own-posts/last-synced-at",
      );
      const n = Number(data.last_synced_at ?? 0);
      return n > 0 ? n : null;
    },
    async sync(accountId) {
      const data = await apiRequest<{ list: Record<string, unknown>[] }>("/api/v1/own-posts/sync", {
        method: "POST",
        body: { account_id: accountId },
      });
      return (data.list ?? []).map(mapOwnPost);
    },
    async loadReplies(postId) {
      const raw = await apiRequest<Record<string, unknown>>("/api/v1/own-posts/load-replies", {
        method: "POST",
        body: { post_id: postId },
      });
      return mapOwnPost(raw);
    },
    async generateReply(opts) {
      const data = await apiRequest<{ text: string }>("/api/v1/own-posts/generate-reply", {
        method: "POST",
        body: {
          post_id: opts.postId,
          reply_id: opts.replyId,
          persona_id: opts.personaId,
        },
      });
      return String(data.text ?? "");
    },
    async sendReply(opts) {
      const raw = await apiRequest<Record<string, unknown>>("/api/v1/own-posts/send-reply", {
        method: "POST",
        body: {
          post_id: opts.postId,
          reply_id: opts.replyId,
          text: opts.text,
          account_id: opts.accountId,
          image_urls: opts.imageUrls,
        },
      });
      return mapOwnPost(raw);
    },
    async manageReply(opts) {
      const raw = await apiRequest<Record<string, unknown>>("/api/v1/own-posts/manage-reply", {
        method: "POST",
        body: { post_id: opts.postId, reply_id: opts.replyId, hide: opts.hide },
      });
      return mapOwnPost(raw);
    },
    async setReplyControl(opts) {
      const raw = await apiRequest<Record<string, unknown>>("/api/v1/own-posts/reply-control", {
        method: "POST",
        body: { post_id: opts.postId, reply_control: opts.replyControl },
      });
      return mapOwnPost(raw);
    },
    async analyzePost(postId) {
      const raw = await apiRequest<Record<string, unknown>>("/api/v1/own-posts/analyze", {
        method: "POST",
        body: { post_id: postId },
      });
      return mapOwnPost(raw);
    },
    async generateFromFormula() {
      throw new Error("generateFromFormula 已移除（OP-11）");
    },
  };
}

function createLiveMentions(): MentionsRepo {
  return {
    async list(accountId) {
      const q = accountId ? `?account_id=${encodeURIComponent(accountId)}` : "";
      const data = await apiRequest<{ list: Record<string, unknown>[] }>(
        `/api/v1/mentions${q}`,
      );
      return (data.list ?? []).map(mapMention);
    },
    async sync(accountId) {
      const data = await apiRequest<{ list: Record<string, unknown>[] }>("/api/v1/mentions/sync", {
        method: "POST",
        body: { account_id: accountId },
      });
      return (data.list ?? []).map(mapMention);
    },
    async generateReply(id, personaId) {
      const raw = await apiRequest<Record<string, unknown>>(
        `/api/v1/mentions/${encodeURIComponent(id)}/generate-reply`,
        { method: "POST", body: { persona_id: personaId } },
      );
      return mapMention(raw);
    },
    async markReplied(id, text, imageUrls) {
      const raw = await apiRequest<Record<string, unknown>>(
        `/api/v1/mentions/${encodeURIComponent(id)}/mark-replied`,
        { method: "POST", body: { text, image_urls: imageUrls } },
      );
      return mapMention(raw);
    },
    async skip(id) {
      const raw = await apiRequest<Record<string, unknown>>(
        `/api/v1/mentions/${encodeURIComponent(id)}/skip`,
        { method: "POST", body: {} },
      );
      return mapMention(raw);
    },
  };
}

export function createLiveRepos(): Repos {
  return {
    dataSource: "live",
    auth: createLiveAuth(),
    adminUsers: createLiveAdmin(),
    settings: createLiveSettings(),
    accounts: createLiveAccounts(),
    jobs: createLiveJobs(),
    notifications: createLiveNotifications(),
    usage: createLiveUsage(),
    invite: createLiveInviteRepo(),
    personas: createLivePersonas(),
    plays: createLivePlays(),
    outbox: createLiveOutbox(),
    compose: createLiveCompose(),
    ownPosts: createLiveOwnPosts(),
    mentions: createLiveMentions(),
    inspiration: createLiveInspiration(),
    research: createLiveResearch(),
    media: createLiveMedia(),
    scout: createLiveScout(),
    growth: createLiveGrowthRepo(),
    radar: createLiveRadarRepo(),
    crm: createLiveCrmRepo(),
  };
}
