const TOKEN_KEY = "eth_hedge_token";
const USER_KEY = "eth_hedge_user";
const TOKEN_EXP_KEY = "eth_hedge_token_exp";

export function getApiBase(): string {
  return window.location.origin;
}

export function getToken(): string | null {
  return localStorage.getItem(TOKEN_KEY);
}

export function setSession(token: string, username: string, expiresInSec?: number) {
  localStorage.setItem(TOKEN_KEY, token);
  localStorage.setItem(USER_KEY, username);
  if (expiresInSec != null && expiresInSec > 0) {
    localStorage.setItem(
      TOKEN_EXP_KEY,
      String(Date.now() + expiresInSec * 1000),
    );
  }
}

export function clearSession() {
  localStorage.removeItem(TOKEN_KEY);
  localStorage.removeItem(USER_KEY);
  localStorage.removeItem(TOKEN_EXP_KEY);
  localStorage.removeItem("eth_hedge_api_base");
}

export function getUsername(): string | null {
  return localStorage.getItem(USER_KEY);
}

/** 与企微通知同一机器名（公开接口，登录前可用）。 */
export async function fetchMachineName(): Promise<string> {
  try {
    const res = await fetch(`${getApiBase()}/api/auth/branding`);
    if (!res.ok) return "";
    const data = (await res.json()) as { machine_name?: string };
    return String(data.machine_name || "").trim();
  } catch {
    return "";
  }
}

function tokenExpiresAtMs(): number | null {
  const raw = localStorage.getItem(TOKEN_EXP_KEY);
  if (!raw) return null;
  const n = Number(raw);
  return Number.isFinite(n) ? n : null;
}

/** 距过期不足该毫秒则主动换发（默认 12h）。 */
const REFRESH_BEFORE_MS = 12 * 60 * 60 * 1000;

let _refreshInFlight: Promise<boolean> | null = null;

/**
 * 用当前 Bearer 换发新 HMAC token（非 Flask session）。
 * 登录态下由定时器/页面可见时静默调用。
 */
export async function refreshAuthToken(): Promise<boolean> {
  const token = getToken();
  if (!token) return false;
  if (_refreshInFlight) return _refreshInFlight;
  _refreshInFlight = (async () => {
    try {
      const res = await fetch(`${getApiBase()}/api/auth/refresh`, {
        method: "POST",
        headers: {
          Authorization: `Bearer ${token}`,
          "Content-Type": "application/json",
        },
        body: "{}",
      });
      if (!res.ok) return false;
      const data = (await res.json()) as LoginResult;
      if (!data?.token) return false;
      setSession(data.token, data.username || getUsername() || "", data.expires_in);
      return true;
    } catch {
      return false;
    } finally {
      _refreshInFlight = null;
    }
  })();
  return _refreshInFlight;
}

let _lastRefreshAttemptMs = 0;

/** 临近过期则刷新；返回是否仍持有有效 token。 */
export async function ensureFreshToken(): Promise<boolean> {
  if (!getToken()) return false;
  const exp = tokenExpiresAtMs();
  const need =
    exp == null || exp - Date.now() < REFRESH_BEFORE_MS;
  if (!need) return true;
  const now = Date.now();
  // 无过期戳的旧会话：最多每 60s 尝试一次，避免轮询打爆 /refresh
  if (exp == null && now - _lastRefreshAttemptMs < 60_000) {
    return true;
  }
  _lastRefreshAttemptMs = now;
  const ok = await refreshAuthToken();
  if (!ok && exp != null && exp <= now) {
    clearSession();
    return false;
  }
  return !!getToken();
}

/** 在已登录壳内启动：定时换发 + 页面回到前台时检查。 */
export function startAuthTokenAutoRefresh(): () => void {
  const tick = () => {
    void ensureFreshToken();
  };
  tick();
  const id = window.setInterval(tick, 30 * 60 * 1000);
  const onVis = () => {
    if (document.visibilityState === "visible") tick();
  };
  document.addEventListener("visibilitychange", onVis);
  return () => {
    window.clearInterval(id);
    document.removeEventListener("visibilitychange", onVis);
  };
}

export async function apiFetch<T>(
  path: string,
  options: RequestInit = {},
): Promise<T> {
  if (
    !path.startsWith("/api/auth/login") &&
    !path.startsWith("/api/auth/fleet-exchange")
  ) {
    await ensureFreshToken();
  }
  const base = getApiBase();
  const headers = new Headers(options.headers || {});
  if (!headers.has("Content-Type") && options.body) {
    headers.set("Content-Type", "application/json");
  }
  const token = getToken();
  if (token) headers.set("Authorization", `Bearer ${token}`);

  const res = await fetch(`${base}${path}`, { ...options, headers });
  if (res.status === 401) {
    clearSession();
    throw new Error("unauthorized");
  }
  const text = await res.text();
  let data: unknown = null;
  try {
    data = text ? JSON.parse(text) : null;
  } catch {
    data = { detail: text };
  }
  if (!res.ok) {
    const detail =
      typeof data === "object" && data && "detail" in data
        ? String((data as { detail: unknown }).detail)
        : res.statusText;
    throw new Error(detail || `HTTP ${res.status}`);
  }
  return data as T;
}

export type LoginResult = {
  token: string;
  username: string;
  expires_in: number;
  env_name: string;
  mode: string;
};

export async function login(username: string, password: string) {
  const res = await apiFetch<LoginResult>("/api/auth/login", {
    method: "POST",
    body: JSON.stringify({ username, password }),
  });
  setSession(res.token, res.username, res.expires_in);
  return res;
}

export async function changeCredentials(input: {
  current_password: string;
  new_username: string;
  new_password: string;
}) {
  const res = await apiFetch<LoginResult>("/api/auth/change-credentials", {
    method: "POST",
    body: JSON.stringify(input),
  });
  setSession(res.token, res.username, res.expires_in);
  return res;
}

export type MarketSnapshot = {
  connected: boolean;
  updated_at_ms: number | null;
  index_px: number | null;
  exchange?: string;
  perp_inst_id?: string;
  pair: {
    expiry_ymd: string;
    strike: number;
    call_inst_id: string;
    put_inst_id: string;
  } | null;
  perp: Quote | null;
  call: Quote | null;
  put: Quote | null;
  ask_compare: {
    call_ask: number | null;
    put_ask: number | null;
    bias: string;
  };
};

type Quote = {
  inst_id: string;
  bid: number | null;
  ask: number | null;
  bid_sz: number | null;
  ask_sz: number | null;
  mark_px: number | null;
};

export type PlanState = {
  running: boolean;
  phase: string;
  rounds_done: number;
  window_key: string | null;
  rest_left_sec: number;
  rest_seconds: number;
  skip_weekends?: boolean;
  one_expiry_per_day?: boolean;
  exit_move_pct?: number;
  exit_mode: "fixed_usdt" | "premium_multiple";
  net_profit_target: number;
  premium_exit_multiple: number;
  exit_target_usdt: number;
  leverage: number;
  perp_margin_mode?: "cross" | "isolated";
  perp_qty_eth?: number;
  option_qty_eth?: number;
  min_option_hours: number;
  min_option_leverage: number;
  atm_open_offset_enabled?: boolean;
  max_atm_open_offset?: number;
  fixed_direction_enabled?: boolean;
  fixed_perp_side?: "long" | "short";
  can_open: boolean;
  open_capacity?: {
    leverage?: number;
    perp_can_open?: boolean | null;
    option_can_open?: boolean | null;
    perp_label?: string;
    option_label?: string;
    funds_ok?: boolean;
    perp_need_usdt?: number | null;
    option_need_usdc?: number | null;
    perp_have_usdt?: number | null;
    option_have_usdc?: number | null;
  };
  last_error: string | null;
  show_manual_trade_buttons?: boolean;
  position: {
    has_position: boolean;
    group_id?: string;
    open_at_ms?: number | null;
    perp_side?: string;
    option_side?: string;
    perp_inst_id?: string;
    perp_entry_px?: number;
    perp_qty_eth?: number;
    perp_mark_px?: number | null;
    perp_notional?: number;
    perp_margin?: number | null;
    leverage?: number;
    option_inst_id?: string;
    option_entry_px?: number;
    option_qty_eth?: number;
    option_qty_contracts?: number;
    option_mark_px?: number | null;
    option_bid_sz?: number | null;
    option_leverage?: number | null;
    strike?: number | null;
    expiry_ymd?: string | null;
    expiry_ms?: number | null;
    perp_upl?: number;
    option_upl?: number;
    fees_paid?: number;
    funding_usdt?: number;
    est_close_fees?: number;
    net_pnl?: number;
    pnl_source?: string;
    index_px?: number | null;
    entry_index_px?: number;
    move_points?: number;
    move_pct?: number;
    initial_premium?: number;
    premium_gap?: number;
  };
  residuals?: {
    group_id: string;
    option_inst_id: string;
    option_side: string;
    strike: number | null;
    expiry_ymd: string | null;
    status: string;
  }[];
  ledger: { equity: number; available: number; reserved: number };
  mode?: "SIM" | "LIVE";
  sim?: boolean;
  live_ready?: boolean;
  live_ready_reason?: string;
};

export type StrategySettings = {
  fee_rate: number;
  exit_mode: "fixed_usdt" | "premium_multiple";
  net_profit_target: number;
  premium_exit_multiple: number;
  rest_seconds: number;
  live_order_interval_sec?: number;
  skip_weekends?: boolean;
  one_expiry_per_day?: boolean;
  initial_equity?: number;
  leverage?: number;
  perp_margin_mode?: "cross" | "isolated";
  min_option_hours?: number;
  min_option_leverage?: number;
  atm_open_offset_enabled?: boolean;
  max_atm_open_offset?: number;
  fixed_direction_enabled?: boolean;
  fixed_perp_side?: "long" | "short";
  close_bid_mark_max_pct?: number;
  perp_qty_eth?: number;
  option_qty_eth?: number;
  show_manual_trade_buttons?: boolean;
  sizing_mode?: "manual" | "risk_based";
  risk_leverage_basis?: "actual" | "selection";
  risk_loss_mode?: "percent" | "absolute";
  risk_loss_pct?: number;
  risk_loss_usdt?: number;
  risk_capital_source?: "trading_account" | "manual";
  risk_manual_capital_usdt?: number;
  risk_perp_unit?: number;
  risk_option_unit?: number;
  risk_exit_unit?: number;
  risk_sizing_preview?: Record<string, unknown>;
  exchange?: string;
};

export type RuntimeSettings = {
  mode: "SIM" | "LIVE";
  exchange: string;
  okx_configured: boolean;
  binance_configured: boolean;
  okx_api_key_masked: string | null;
  okx_api_secret_masked: string | null;
  okx_api_passphrase_masked: string | null;
  binance_api_key_masked: string | null;
  binance_api_secret_masked: string | null;
  live_ready: boolean;
  live_ready_reason: string;
  sim: boolean;
};

export type NotifySettings = {
  enabled: boolean;
  webhook_configured: boolean;
  webhook_url_masked: string | null;
  venue_label: string;
  machine_name?: string;
};

export async function downloadBackup(name: string): Promise<void> {
  await ensureFreshToken();
  const token = getToken();
  const res = await fetch(
    `${getApiBase()}/api/backup/download/${encodeURIComponent(name)}`,
    {
      headers: token ? { Authorization: `Bearer ${token}` } : {},
    },
  );
  if (res.status === 401) {
    clearSession();
    throw new Error("unauthorized");
  }
  if (!res.ok) {
    const text = await res.text();
    let detail = res.statusText;
    try {
      const j = JSON.parse(text) as { detail?: string };
      if (j.detail) detail = String(j.detail);
    } catch {
      if (text) detail = text;
    }
    throw new Error(detail || `HTTP ${res.status}`);
  }
  const blob = await res.blob();
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = name;
  document.body.appendChild(a);
  a.click();
  a.remove();
  URL.revokeObjectURL(url);
}

export async function uploadRestoreBackup(
  file: File,
  confirmPhrase: string,
): Promise<{ detail?: string; restart_required?: boolean }> {
  await ensureFreshToken();
  const token = getToken();
  const res = await fetch(`${getApiBase()}/api/backup/restore`, {
    method: "POST",
    headers: {
      ...(token ? { Authorization: `Bearer ${token}` } : {}),
      "X-Confirm-Phrase": confirmPhrase,
      "Content-Type": "application/zip",
    },
    body: file,
  });
  if (res.status === 401) {
    clearSession();
    throw new Error("unauthorized");
  }
  const text = await res.text();
  let data: unknown = null;
  try {
    data = text ? JSON.parse(text) : null;
  } catch {
    data = { detail: text };
  }
  if (!res.ok) {
    const detail =
      typeof data === "object" && data && "detail" in data
        ? String((data as { detail: unknown }).detail)
        : res.statusText;
    throw new Error(detail || `HTTP ${res.status}`);
  }
  return data as { detail?: string; restart_required?: boolean };
}

export type BackupStatus = {
  auto_enabled: boolean;
  interval_hours: number;
  keep_count: number;
  last_at_ms: number | null;
  backup_dir: string;
  items: {
    name: string;
    path: string;
    size_bytes: number;
    mtime_ms: number;
  }[];
};
