const API_BASE = "";

const TOKEN_KEY = "dtp_auth_token";
const USER_KEY = "dtp_user";

let isRefreshing = false;
let refreshPromise: Promise<string> | null = null;

/**
 * Callback invoked after a successful token refresh so that other
 * modules (e.g., the socket client) can update their stored JWT.
 * Set via `setOnTokenRefreshed()`.
 */
let onTokenRefreshedCallback: ((newToken: string) => void) | null = null;

/**
 * Register a callback to be invoked whenever the JWT is refreshed.
 * Used by the socket client to update its auth token for reconnections.
 */
export function setOnTokenRefreshed(cb: (newToken: string) => void): void {
  onTokenRefreshedCallback = cb;
}

export interface ApiResponse<T = unknown> {
  success: boolean;
  message?: string;
  user?: T;
  token?: string;
  data?: T;
}

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

function setToken(token: string): void {
  localStorage.setItem(TOKEN_KEY, token);
}

function signOut(): void {
  localStorage.removeItem(TOKEN_KEY);
  localStorage.removeItem(USER_KEY);
  window.location.href = "/";
}

/**
 * Check if the current JWT's `exp` claim is within the refresh threshold.
 * Decodes the payload (base64) without cryptographic verification.
 * Returns true if the token will expire within `marginMs` milliseconds.
 */
function isTokenExpiringSoon(token: string, marginMs = 5 * 60 * 1000): boolean {
  try {
    const parts = token.split(".");
    if (parts.length < 2) return true;
    const payload = JSON.parse(atob(parts[1]!));
    if (!payload.exp) return true;
    const expiresAt = payload.exp * 1000; // seconds → ms
    return Date.now() > (expiresAt - marginMs);
  } catch {
    return true; // if we can't decode it, treat it as expiring
  }
}

async function refreshAuthToken(): Promise<string> {
  const response = await fetch(`${API_BASE}/api/v1/auth/renew-token`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    credentials: "include",
  });

  if (!response.ok) {
    throw new Error(`Token refresh failed: ${response.status}`);
  }

  const text = await response.text();
  try {
    const json = JSON.parse(text) as ApiResponse & { token?: string; data?: { token?: string } };
    if (!json.success) {
      throw new Error(json.message || "Token refresh failed");
    }
    /*
     * The API v1 renew-token endpoint returns { success: true, token: "..." }
     * at the top level (not nested under `data`). Handle both formats for
     * robustness: check json.token first, then fall back to json.data?.token.
     */
    const newToken = json.token ?? json.data?.token;
    if (!newToken) {
      throw new Error("Token refresh response missing token");
    }
    return newToken;
  } catch (err) {
    if (err instanceof Error && err.message.includes("Token refresh")) throw err;
    throw new Error(`Invalid refresh response: ${text.slice(0, 200)}`);
  }
}

async function request<T>(
  method: string,
  path: string,
  body?: Record<string, unknown>,
  retryCount = 0,
): Promise<T> {
  let token = getToken();

  /*
   * Proactive token refresh: if the JWT's `exp` claim shows it will expire
   * within 5 minutes, refresh it before making the request. This avoids
   * unnecessary 401 errors and the resulting socket disconnections.
   */
  if (token && isTokenExpiringSoon(token)) {
    try {
      if (!isRefreshing) {
        isRefreshing = true;
        refreshPromise = refreshAuthToken();
      }
      token = (await refreshPromise)!;
      setToken(token);
      onTokenRefreshedCallback?.(token);
      isRefreshing = false;
      refreshPromise = null;
    } catch {
      isRefreshing = false;
      refreshPromise = null;
      // Don't sign out on proactive refresh failure — the token may still
      // be valid at the DB level even if the JWT exp is close. Let the
      // reactive 401 handler below deal with it if needed.
    }
  }

  const headers: Record<string, string> = {
    "Content-Type": "application/json",
  };

  if (token) {
    headers["Authorization"] = `Bearer ${token}`;
  }

  const options: RequestInit = {
    method,
    headers,
    credentials: "include",
  };

  if (body) {
    options.body = JSON.stringify(body);
  }

  const response = await fetch(`${API_BASE}${path}`, options);

  if (response.status === 401 && retryCount === 0) {
    try {
      if (!isRefreshing) {
        isRefreshing = true;
        refreshPromise = refreshAuthToken();
      }
      
      const newToken = (await refreshPromise)!;
      setToken(newToken);
      onTokenRefreshedCallback?.(newToken);
      isRefreshing = false;
      refreshPromise = null;

      return request<T>(method, path, body, retryCount + 1);
    } catch {
      isRefreshing = false;
      refreshPromise = null;
      signOut();
      throw new Error("Session expired. Please sign in again.");
    }
  }

  const text = await response.text();

  if (!text) {
    throw new Error("Empty response");
  }

  try {
    var json = JSON.parse(text) as ApiResponse<T>;
  } catch {
    throw new Error(`Invalid JSON: ${text.slice(0, 200)}`);
  }

  if (!json.success) {
    throw new Error(json.message || "Request failed");
  }

  if (json.token !== undefined && json.user !== undefined) {
    return json as T;
  }
  if (json.data !== undefined) {
    return json.data as T;
  }
  return json as T;
}

export const api = {
  get: <T>(path: string) => request<T>("GET", path),
  post: <T>(path: string, body?: Record<string, unknown>) =>
    request<T>("POST", path, body),
  put: <T>(path: string, body: Record<string, unknown>) =>
    request<T>("PUT", path, body),
  delete: <T>(path: string) => request<T>("DELETE", path),
};

export interface User {
  _id: string;
  email: string;
  displayName: string;
  flags: string[];
  persona?: string;
}

export interface AuthResponse {
  user: User;
  token: string;
}

export interface ProjectSkill {
  _id: string;
  name: string;
  content: string;
  modes: string[];
}

export interface ProjectTask {
  _id: string;
  name: string;
  provider: string;
  selectedModel: string;
  mode: string;
  crontab: string;
  content: string;
  enabled: boolean;
  lastRun?: string;
}

export interface Project {
  _id: string;
  createdAt: string;
  user: string;
  status: "active" | "inactive" | "archived";
  name: string;
  slug: string;
  gitUrl?: string;
  description?: string;
  system?: string;
  skills: ProjectSkill[];
  tasks: ProjectTask[];
}

export const userApi = {
  updateSettings: (data: {
    displayName?: string;
    currentPassword?: string;
    password?: string;
    persona?: string;
  }) => api.put<User>("/api/v1/user/settings", data),
};

export const projectApi = {
  getAll: () => api.get<Project[]>("/api/v1/projects"),
  get: (id: string) => api.get<Project>(`/api/v1/projects/${id}`),
  create: (data: { name: string; slug: string; gitUrl?: string }) =>
    api.post<Project>("/api/v1/projects", data),
  pull: (gitUrl: string) =>
    api.post<Project>("/api/v1/projects/pull", { gitUrl }),
  update: (
    id: string,
    data: Partial<{
      name: string;
      slug: string;
      gitUrl: string;
      status: string;
      description: string;
      system: string;
      skills: ProjectSkill[];
      tasks: ProjectTask[];
    }>,
  ) => api.put<Project>(`/api/v1/projects/${id}`, data),
  delete: (id: string) => api.delete<void>(`/api/v1/projects/${id}`),
};

export interface SubProcessStat {
  pid: number;
  command: string;
  args: string[];
  projectId: string;
  projectSlug: string;
  projectName: string;
  status: "running" | "stopped" | "error";
  createdAt: string;
  updatedAt: string;
  stdoutFileName: string;
  stderrFileName: string;
}

export interface DroneRegistration {
  _id: string;
  hostname: string;
  workspaceDir: string;
  status: "starting" | "available" | "busy" | "offline";
  user: string;
  createdAt: string;
  updatedAt: string;
}

export const droneApi = {
  getAll: () => api.get<DroneRegistration[]>("/api/v1/drone/registration"),
  getForManager: () =>
    api.get<DroneRegistration[]>("/api/v1/drone/registration/manager"),
  terminate: (registrationId: string) =>
    api.post<ApiResponse>(
      `/api/v1/drone/registration/${registrationId}/terminate`,
    ),
};

export interface AiModelCapabilities {
  canCallTools: boolean;
  hasVision: boolean;
  hasEmbedding: boolean;
  hasThinking: boolean;
  isInstructTuned: boolean;
}

export interface AiModelSettings {
  temperature?: number;
  topP?: number;
  topK?: number;
  numCtx?: number;
}

export interface AiModel {
  id: string;
  name: string;
  parameterCount?: number;
  parameterLabel?: string;
  contextWindow?: number;
  capabilities: AiModelCapabilities;
  settings?: AiModelSettings;
}

export interface AiProvider {
  _id: string;
  name: string;
  apiType: "ollama" | "openai";
  baseUrl: string;
  enabled: boolean;
  models: AiModel[];
  lastModelRefresh: string;
}

export const providerApi = {
  getAll: () => api.get<AiProvider[]>("/api/v1/providers"),
  get: (id: string) => api.get<AiProvider>(`/api/v1/providers/${id}`),
};

export enum ChatSessionMode {
  Plan = "plan",
  Build = "build",
  Test = "test",
  Ship = "ship",
  Develop = "dev",
}

export interface ChatSessionStats {
  turnCount: number;
  toolCallCount: number;
  inputTokens: number;
  outputTokens: number;
}

export interface ChatSession {
  _id: string;
  createdAt: string;
  lastMessageAt?: string;
  user: string | any;
  project: string | any;
  name: string;
  mode: ChatSessionMode;
  provider: string | AiProvider;
  selectedModel: string;
  reasoningEffort?: string;
  numCtx?: number;
  stats: ChatSessionStats;
  pins: Array<{ _id?: string; content: string }>;
}

export interface ChatTurnStats {
  toolCallCount: number;
  inputTokens: number;
  thinkingTokenCount: number;
  responseTokens: number;
  durationMs: number;
  durationLabel: string;
}

export interface ChatTurnPrompts {
  user: string;
  system?: string;
}

export interface ChatTurnBlockThinking {
  mode: 'thinking';
  createdAt: string;
  content: string;
}

export interface ChatTurnBlockResponding {
  mode: 'responding';
  createdAt: string;
  content: string;
}

export interface ChatTurnBlockTool {
  mode: 'tool';
  createdAt: string;
  content: {
    callId: string;
    name: string;
    parameters?: string;
    response?: string;
    subagent?: {
      agentId: string;
      thinking: string;
      response: string;
      toolCalls: Array<{
        callId: string;
        name: string;
        parameters?: string;
        response?: string;
      }>;
      stats: ChatTurnStats;
    };
  };
}

export type ChatTurnBlock = ChatTurnBlockThinking | ChatTurnBlockResponding | ChatTurnBlockTool;

export interface ChatTurn {
  _id: string;
  createdAt: string;
  user: string | any;
  project: string | any;
  session: string | ChatSession;
  provider: string | AiProvider;
  llm: string;
  numCtx?: number;
  mode: ChatSessionMode;
  status: "processing" | "finished" | "aborted" | "error";
  prompts: ChatTurnPrompts;
  blocks: ChatTurnBlock[];
  errorMessage?: string;
  toolCalls: Array<{
    callId: string;
    name: string;
    parameters?: string;
    response?: string;
    subagent?: ChatTurnBlockTool['content']['subagent'];
  }>;
  subagents: any[];
  stats: ChatTurnStats;
}

export const chatSessionApi = {
  getAll: (projectId?: string) =>
    api.get<ChatSession[]>(
      `/api/v1/chat-sessions${projectId ? `?projectId=${projectId}` : ""}`,
    ),
  get: (id: string) => api.get<ChatSession>(`/api/v1/chat-sessions/${id}`),
  create: (data: {
    projectId: string;
    providerId: string;
    selectedModel: string;
    mode?: ChatSessionMode;
    name?: string;
  }) => api.post<ChatSession>("/api/v1/chat-sessions", data),
  update: (id: string, data: Partial<ChatSession>) =>
    api.put<ChatSession>(`/api/v1/chat-sessions/${id}`, data),
  delete: (id: string) => api.delete<void>(`/api/v1/chat-sessions/${id}`),
  getTurns: (id: string) =>
    api.get<ChatTurn[]>(`/api/v1/chat-sessions/${id}/turns`),
};
