// Thin client for the self-hosted Node backend documented in
// docs/BACKEND_BLUEPRINT.md. It handles REST calls (Delta account mapping,
// approvals, manual close) and a private WebSocket that streams positions,
// orders, signals, and balance updates for the current user.

export const API_BASE_URL =
  (import.meta.env.VITE_API_BASE_URL as string | undefined) ?? "http://localhost:4000/api";
const WS_URL =
  (import.meta.env.VITE_BACKEND_WS_URL as string | undefined) ??
  API_BASE_URL.replace(/^http/, "ws").replace(/\/api\/?$/, "") + "/ws";

// ---------------- Types ----------------

export type OrderStatus =
  | "PENDING_APPROVAL"
  | "SUBMITTED"
  | "PARTIALLY_FILLED"
  | "FILLED"
  | "CANCELED"
  | "REJECTED";

export interface DeltaPosition {
  _id: string;
  coin_symbol: string;
  position_side: "LONG" | "SHORT";
  entry_price: number;
  current_price: number;
  size: number;
  leverage: number;
  stop_loss: number;
  take_profit: number;
  max_wait_time_minutes: number;
  opened_at: string;
  pnl: number;
  pnl_pct: number;
  order_status: OrderStatus;
  fill_price?: number;
  filled_size?: number;
  order_id?: string;
  execution_mode?: "paper" | "live";
  algo?: string;
  algo_name?: string;
}

export interface TradeSignal {
  _id: string;
  coin_symbol: string;
  position_side: "LONG" | "SHORT";
  entry_price: number;
  stop_loss: number;
  take_profit: number;
  order_sizing_type: "FIXED_AMOUNT" | "PERCENTAGE";
  order_size_value: number;
  leverage: number;
  confidence: number; // 0..1
  rationale: string;
  indicators: { rsi?: number; macd?: string; ema?: string };
  created_at: string;
  expires_at: string;
  algo?: string;
  algo_name?: string;
  timeframe?: string;
}

export interface DeltaAccount {
  connected: boolean;
  account_id?: string;
  email?: string;
  balance?: number;
  currency?: string;
  permissions?: string[];
  last_synced_at?: string;
}

export type BackendEvent =
  | { type: "position.update"; position: DeltaPosition }
  | { type: "position.close"; position_id: string }
  | { type: "order.update"; position_id: string; status: OrderStatus; fill_price?: number; filled_size?: number }
  | { type: "signal.new"; signal: TradeSignal }
  | { type: "signal.expired"; signal_id: string }
  | { type: "account.update"; account: DeltaAccount };

// ---------------- REST ----------------

async function req<T>(path: string, init: RequestInit = {}): Promise<T> {
  const token =
    typeof window !== "undefined" ? window.localStorage.getItem("delta-terminal-token") : null;
  const res = await fetch(`${API_BASE_URL}${path}`, {
    ...init,
    headers: {
      "Content-Type": "application/json",
      ...(token ? { Authorization: `Bearer ${token}` } : {}),
      ...(init.headers ?? {}),
    },
  });
  if (!res.ok) throw new Error(`${res.status} ${await res.text().catch(() => res.statusText)}`);
  return (res.status === 204 ? (null as unknown as T) : ((await res.json()) as T));
}

export const backendApi = {
  health() {
    return req<{ ok: true; driver: string; ts: string }>("/health", { method: "GET" });
  },
  saveDeltaCredentials(body: { api_key: string; api_secret: string; account_label?: string }) {
    return req<{ ok: true; account: DeltaAccount }>("/delta/credentials", {
      method: "POST",
      body: JSON.stringify(body),
    });
  },
  testDeltaCredentials() {
    return req<DeltaAccount>("/delta/account", { method: "GET" });
  },
  listPositions() {
    return req<DeltaPosition[]>("/positions", { method: "GET" });
  },
  listSignals() {
    return req<TradeSignal[]>("/signals?status=pending", { method: "GET" });
  },
  approveSignal(id: string, overrides?: { order_sizing_type?: "FIXED_AMOUNT" | "PERCENTAGE"; order_size_value?: number; leverage?: number }) {
    return req<{ ok: true; position_id: string }>(`/signals/${id}/approve`, {
      method: "POST",
      body: JSON.stringify(overrides ?? {}),
    });
  },
  rejectSignal(id: string, reason?: string) {
    return req<{ ok: true }>(`/signals/${id}/reject`, {
      method: "POST",
      body: JSON.stringify({ reason }),
    });
  },
  closePosition(id: string) {
    return req<{ ok: true }>(`/positions/${id}/close`, { method: "POST" });
  },
  placeManualTrade(body: {
    coin_symbol: string;
    position_side: "LONG" | "SHORT";
    entry_price: number;
    stop_loss: number;
    take_profit: number;
    order_size_value: number;
    leverage?: number;
    order_sizing_type?: "FIXED_AMOUNT" | "PERCENTAGE";
  }) {
    return req<{ ok: true; position_id: string; position: DeltaPosition }>(
      "/positions/manual",
      { method: "POST", body: JSON.stringify(body) },
    );
  },

  updatePositionSlTp(id: string, body: { stop_loss: number; take_profit: number }) {
    return req<{ ok: true; position: DeltaPosition }>(`/positions/${id}/sl-tp`, {
      method: "POST",
      body: JSON.stringify(body),
    });
  },
  listAudit(params: { actor?: string; subject?: string; action?: string; limit?: number } = {}) {
    const qs = new URLSearchParams(
      Object.entries(params).filter(([, v]) => v != null).map(([k, v]) => [k, String(v)]),
    ).toString();
    return req<Array<{
      id: string; actor_user_id: string | null; subject_user_id: string | null;
      actor_email: string | null; subject_email: string | null;
      impersonating: boolean; action: string; meta: Record<string, unknown>; ts: string;
    }>>(`/admin/audit${qs ? `?${qs}` : ""}`, { method: "GET" });
  },
  getAdminSettings() {
    return req<AdminSettings>("/settings/admin", { method: "GET" });
  },
  saveAdminSettings(body: Partial<AdminSettingsPatch>) {
    return req<AdminSettings>("/settings/admin", { method: "POST", body: JSON.stringify(body) });
  },
  listSignalHistory(params: SignalHistoryQuery = {}) {
    const qs = new URLSearchParams(
      Object.entries(params).filter(([, v]) => v != null && v !== "").map(([k, v]) => [k, String(v)]),
    ).toString();
    return req<SignalHistoryResponse>(`/signals/history${qs ? `?${qs}` : ""}`, { method: "GET" });
  },
  listAlgoStats(params: AlgoStatsQuery = {}) {
    const qs = new URLSearchParams(
      Object.entries(params).filter(([, v]) => v != null && v !== "").map(([k, v]) => [k, String(v)]),
    ).toString();
    return req<AlgoStatsResponse>(`/signals/algo-stats${qs ? `?${qs}` : ""}`, { method: "GET" });
  },
  getLiveTrading() {
    return req<{ ok: true; live_trading_enabled: boolean; has_credentials: boolean; env_enabled: boolean }>(
      "/delta/live-trading",
      { method: "GET" },
    );
  },
  setLiveTrading(enabled: boolean) {
    return req<{ ok: true; live_trading_enabled: boolean; env_enabled: boolean }>(
      "/delta/live-trading",
      { method: "POST", body: JSON.stringify({ enabled }) },
    );
  },
};

export type SignalOutcome =
  | "target_hit" | "stop_hit" | "expired" | "rejected" | "approved" | "pending";

export interface SignalHistoryItem extends TradeSignal {
  status: "pending" | "approved" | "rejected" | "expired";
  outcome: SignalOutcome;
  resolved_outcome?: "target_hit" | "stop_hit" | "expired";
  reject_reason?: string;
  algo?: string;
  algo_name?: string;
}
export interface SignalHistoryQuery {
  q?: string; side?: "LONG" | "SHORT" | "";
  outcome?: SignalOutcome | "";
  from?: string; to?: string;
  algo?: string;              // comma-separated algo ids
  minConfidence?: number;     // 0..100 percent
  sort?: "created_at" | "coin_symbol" | "outcome" | "confidence" | "algo";
  dir?: "asc" | "desc";
  page?: number; limit?: number;
}
export interface SignalHistoryStats {
  total: number; target_hit: number; stop_hit: number; expired: number;
  rejected: number; approved: number; pending: number; resolved: number;
  hit_rate_pct: number; win_over_total_pct: number;
}
export interface SignalHistoryResponse {
  items: SignalHistoryItem[]; total: number; page: number; pageSize: number;
  stats: SignalHistoryStats;
}

export interface AlgoStatsQuery {
  q?: string; side?: "LONG" | "SHORT" | "";
  from?: string; to?: string; minConfidence?: number;
}
export interface AlgoStatItem {
  algo: string; algo_name: string;
  total: number; target_hit: number; stop_hit: number; expired: number;
  pending: number; resolved: number;
  avg_confidence_pct: number; hit_rate_pct: number; win_over_total_pct: number;
}
export interface AlgoStatsResponse { items: AlgoStatItem[] }

export interface AdminSettings {
  branding: { app_title: string; app_description: string; logo_url: string; favicon_url: string; primary_color: string; accent_color: string };
  features: { registration_enabled: boolean; auto_approve_trades: boolean; google_login_enabled: boolean; facebook_login_enabled: boolean; x_login_enabled: boolean };
  oauth: {
    google: { client_id: string; redirect_uri: string; has_secret: boolean };
    facebook: { client_id: string; redirect_uri: string; has_secret: boolean };
    x: { client_id: string; redirect_uri: string; has_secret: boolean };
  };
}
export interface AdminSettingsPatch {
  branding: Partial<AdminSettings["branding"]>;
  features: Partial<AdminSettings["features"]>;
  oauth: Partial<{
    google: { client_id?: string; client_secret?: string; redirect_uri?: string };
    facebook: { client_id?: string; client_secret?: string; redirect_uri?: string };
    x: { client_id?: string; client_secret?: string; redirect_uri?: string };
  }>;
}

// ---------------- WebSocket ----------------

type BackendState = { state: "idle" | "connecting" | "open" | "reconnecting" | "closed"; attempt: number };

export class BackendWs {
  private ws: WebSocket | null = null;
  private listeners = new Set<(e: BackendEvent) => void>();
  private stateListeners = new Set<(s: BackendState) => void>();
  private state: BackendState = { state: "idle", attempt: 0 };
  private timer: number | null = null;
  private stopped = false;
  private heartbeatTimer: number | null = null;
  private lastPongAt = 0;
  private static HEARTBEAT_MS = 25_000;
  private static HEARTBEAT_TIMEOUT_MS = 60_000;

  constructor(private url: string = WS_URL) {}

  onEvent(fn: (e: BackendEvent) => void) {
    this.listeners.add(fn);
    return () => this.listeners.delete(fn);
  }
  onState(fn: (s: BackendState) => void) {
    this.stateListeners.add(fn);
    fn(this.state);
    return () => this.stateListeners.delete(fn);
  }

  start() {
    this.stopped = false;
    if (!this.ws) this.connect();
  }
  stop() {
    this.stopped = true;
    if (this.timer) clearTimeout(this.timer);
    this.clearHeartbeat();
    this.ws?.close();
    this.ws = null;
    this.setState({ state: "closed", attempt: 0 });
  }

  private setState(s: BackendState) {
    this.state = s;
    this.stateListeners.forEach((l) => l(s));
  }

  private clearHeartbeat() {
    if (this.heartbeatTimer) {
      clearInterval(this.heartbeatTimer);
      this.heartbeatTimer = null;
    }
  }

  private startHeartbeat() {
    this.clearHeartbeat();
    this.lastPongAt = Date.now();
    this.heartbeatTimer = window.setInterval(() => {
      if (!this.ws || this.ws.readyState !== 1) return;
      // No pong within the timeout window → treat the socket as dead and force a reconnect.
      if (Date.now() - this.lastPongAt > BackendWs.HEARTBEAT_TIMEOUT_MS) {
        try { this.ws.close(); } catch { /* noop */ }
        return;
      }
      try { this.ws.send(JSON.stringify({ type: "ping", ts: Date.now() })); } catch { /* noop */ }
    }, BackendWs.HEARTBEAT_MS);
  }

  private connect() {
    if (this.stopped) return;
    this.setState({ state: this.state.attempt === 0 ? "connecting" : "reconnecting", attempt: this.state.attempt });
    const token =
      typeof window !== "undefined" ? window.localStorage.getItem("delta-terminal-token") : null;
    const url = token ? `${this.url}?token=${encodeURIComponent(token)}` : this.url;
    try {
      this.ws = new WebSocket(url);
    } catch {
      this.scheduleReconnect();
      return;
    }
    this.ws.addEventListener("open", () => {
      this.setState({ state: "open", attempt: 0 });
      this.startHeartbeat();
    });
    this.ws.addEventListener("message", (e) => {
      try {
        const evt = JSON.parse(typeof e.data === "string" ? e.data : "") as BackendEvent & { type?: string };
        // Any inbound frame counts as liveness; explicit pong updates the watchdog.
        this.lastPongAt = Date.now();
        if ((evt as { type?: string })?.type === "pong") return;
        this.listeners.forEach((l) => l(evt));
      } catch {
        /* ignore */
      }
    });
    this.ws.addEventListener("close", () => {
      this.clearHeartbeat();
      this.scheduleReconnect();
    });
    this.ws.addEventListener("error", () => this.ws?.close());
  }

  private scheduleReconnect() {
    if (this.stopped) return;
    const attempt = this.state.attempt + 1;
    const base = Math.min(30_000, 500 * 2 ** (attempt - 1));
    const delay = base / 2 + Math.random() * base;
    this.setState({ state: "reconnecting", attempt });
    this.timer = window.setTimeout(() => this.connect(), delay);
  }
}


let backendSingleton: BackendWs | null = null;
export function getBackendWs() {
  if (typeof window === "undefined") throw new Error("BackendWs is browser-only");
  if (!backendSingleton) backendSingleton = new BackendWs();
  return backendSingleton;
}
