/**
 * API Client for Mosaic Stack
 * Handles authenticated requests to the backend API
 */

/* eslint-disable @typescript-eslint/no-unsafe-assignment */

import { API_BASE_URL, IS_MOCK_AUTH_MODE } from "../config";

/**
 * In-memory CSRF token storage
 * Using module-level variable instead of localStorage for security
 */
let csrfToken: string | undefined;

export interface ApiError {
  code: string;
  message: string;
  details?: unknown;
}

export interface ApiResponse<T> {
  data: T;
  meta?: {
    total?: number;
    page?: number;
    limit?: number;
  };
}

/** Default timeout for API requests in milliseconds (30 seconds) */
export const DEFAULT_API_TIMEOUT_MS = 30_000;

/**
 * Options for API requests with workspace context
 */
export interface ApiRequestOptions extends RequestInit {
  workspaceId?: string;
  /** Request timeout in milliseconds. Defaults to 30000 (30s). Set to 0 to disable. */
  timeoutMs?: number;
  _isRetry?: boolean; // Internal flag to prevent infinite retry loops
}

const MOCK_ACTIVE_PROJECTS_RESPONSE = [
  {
    id: "project-dev-1",
    name: "Mosaic Stack FE Go-Live",
    status: "active",
    lastActivity: new Date().toISOString(),
    taskCount: 7,
    eventCount: 2,
    color: "#3B82F6",
  },
  {
    id: "project-dev-2",
    name: "Auth Flow Remediation",
    status: "in-progress",
    lastActivity: new Date(Date.now() - 12 * 60_000).toISOString(),
    taskCount: 4,
    eventCount: 0,
    color: "#F59E0B",
  },
] as const;

const MOCK_AGENT_CHAINS_RESPONSE = [
  {
    id: "agent-session-dev-1",
    sessionKey: "dev-session-1",
    label: "UI Validator Agent",
    channel: "codex",
    agentName: "jarvis-agent",
    agentStatus: "WORKING",
    status: "active",
    startedAt: new Date(Date.now() - 42 * 60_000).toISOString(),
    lastMessageAt: new Date(Date.now() - 20_000).toISOString(),
    runtimeMs: 42 * 60_000,
    messageCount: 27,
    contextSummary: "Validating dashboard, tasks, and auth-bypass UX for local development flow.",
  },
  {
    id: "agent-session-dev-2",
    sessionKey: "dev-session-2",
    label: "Telemetry Stub Agent",
    channel: "codex",
    agentName: "jarvis-agent",
    agentStatus: "TERMINATED",
    status: "ended",
    startedAt: new Date(Date.now() - 3 * 60 * 60_000).toISOString(),
    lastMessageAt: new Date(Date.now() - 2 * 60 * 60_000).toISOString(),
    runtimeMs: 63 * 60_000,
    messageCount: 41,
    contextSummary: "Generated telemetry mock payloads for usage and widget rendering.",
  },
] as const;

function getMockApiResponse(endpoint: string, method: string): unknown {
  if (!IS_MOCK_AUTH_MODE || process.env.NODE_ENV !== "development") {
    return undefined;
  }

  if (method === "POST" && endpoint === "/api/widgets/data/active-projects") {
    return [...MOCK_ACTIVE_PROJECTS_RESPONSE];
  }

  if (method === "POST" && endpoint === "/api/widgets/data/agent-chains") {
    return [...MOCK_AGENT_CHAINS_RESPONSE];
  }

  return undefined;
}

/**
 * Fetch CSRF token from the API
 * Token is stored in an httpOnly cookie and returned in response body
 */
export async function fetchCsrfToken(): Promise<string> {
  const url = `${API_BASE_URL}/api/v1/csrf/token`;

  const response = await fetch(url, {
    method: "GET",
    credentials: "include",
  });

  if (!response.ok) {
    const error: ApiError = await response.json().catch(
      (): ApiError => ({
        code: "UNKNOWN_ERROR",
        message: response.statusText || "Failed to fetch CSRF token",
      })
    );

    throw new Error(error.message);
  }

  const data = (await response.json()) as { token: string };
  csrfToken = data.token;
  return data.token;
}

/**
 * Get the current CSRF token from memory
 */
export function getCsrfToken(): string | undefined {
  return csrfToken;
}

/**
 * Clear the CSRF token from memory
 * Useful for testing or after logout
 */
export function clearCsrfToken(): void {
  csrfToken = undefined;
}

/**
 * Ensure CSRF token is available for state-changing requests
 */
async function ensureCsrfToken(): Promise<string> {
  if (!csrfToken) {
    return fetchCsrfToken();
  }
  return csrfToken;
}

/**
 * Make an authenticated API request
 */
export async function apiRequest<T>(endpoint: string, options: ApiRequestOptions = {}): Promise<T> {
  const url = `${API_BASE_URL}${endpoint}`;
  const { workspaceId, timeoutMs, _isRetry, ...fetchOptions } = options;
  const method = (fetchOptions.method ?? "GET").toUpperCase();

  const mockResponse = getMockApiResponse(endpoint, method);
  if (mockResponse !== undefined) {
    return mockResponse as T;
  }

  // Set up abort controller for timeout
  const timeout = timeoutMs ?? DEFAULT_API_TIMEOUT_MS;
  const controller = new AbortController();
  let timeoutId: ReturnType<typeof setTimeout> | undefined;

  if (timeout > 0) {
    timeoutId = setTimeout(() => {
      controller.abort();
    }, timeout);
  }

  // Merge with any caller-provided signal
  const callerSignal = fetchOptions.signal;
  if (callerSignal) {
    callerSignal.addEventListener("abort", () => {
      controller.abort();
    });
  }

  try {
    // Build headers with workspace ID if provided
    const baseHeaders = (fetchOptions.headers as Record<string, string> | undefined) ?? {};
    const headers: Record<string, string> = {
      "Content-Type": "application/json",
      ...baseHeaders,
    };

    // Add workspace ID header — use explicit value, or auto-detect from localStorage
    const resolvedWorkspaceId =
      workspaceId ??
      (typeof window !== "undefined" ? localStorage.getItem("mosaic-workspace-id") : null) ??
      undefined;
    if (resolvedWorkspaceId) {
      headers["X-Workspace-Id"] = resolvedWorkspaceId;
    }

    // Add CSRF token for state-changing requests (POST, PUT, PATCH, DELETE)
    const isStateChanging = ["POST", "PUT", "PATCH", "DELETE"].includes(method);

    if (isStateChanging) {
      const token = await ensureCsrfToken();
      headers["X-CSRF-Token"] = token;
    }

    const response = await fetch(url, {
      ...fetchOptions,
      headers,
      credentials: "include", // Include cookies for session
      signal: controller.signal,
    });

    if (!response.ok) {
      const error: ApiError = await response.json().catch(
        (): ApiError => ({
          code: "UNKNOWN_ERROR",
          message: response.statusText || "An unknown error occurred",
        })
      );

      // Handle CSRF token mismatch - refresh token and retry once
      if (
        response.status === 403 &&
        (error.code === "CSRF_ERROR" || error.message.includes("CSRF")) &&
        !_isRetry
      ) {
        // Refresh CSRF token
        await fetchCsrfToken();

        // Retry the request with new token
        return await apiRequest<T>(endpoint, { ...options, _isRetry: true });
      }

      throw new Error(error.message);
    }

    // 204 No Content responses have no body — return undefined cast to T
    if (response.status === 204) {
      return undefined as T;
    }

    return await (response.json() as Promise<T>);
  } catch (err: unknown) {
    if (err instanceof DOMException && err.name === "AbortError") {
      throw new Error(`Request to ${endpoint} timed out after ${String(timeout)}ms`);
    }
    throw err;
  } finally {
    if (timeoutId !== undefined) {
      clearTimeout(timeoutId);
    }
  }
}

/**
 * GET request helper
 */
export async function apiGet<T>(endpoint: string, workspaceId?: string): Promise<T> {
  const options: ApiRequestOptions = { method: "GET" };
  if (workspaceId !== undefined) {
    options.workspaceId = workspaceId;
  }
  return apiRequest<T>(endpoint, options);
}

/**
 * POST request helper
 */
export async function apiPost<T>(
  endpoint: string,
  data?: unknown,
  workspaceId?: string
): Promise<T> {
  const options: ApiRequestOptions = {
    method: "POST",
  };

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

  if (workspaceId !== undefined) {
    options.workspaceId = workspaceId;
  }

  return apiRequest<T>(endpoint, options);
}

/**
 * PATCH request helper
 */
export async function apiPatch<T>(
  endpoint: string,
  data: unknown,
  workspaceId?: string
): Promise<T> {
  const options: ApiRequestOptions = {
    method: "PATCH",
    body: JSON.stringify(data),
  };

  if (workspaceId !== undefined) {
    options.workspaceId = workspaceId;
  }

  return apiRequest<T>(endpoint, options);
}

/**
 * DELETE request helper
 */
export async function apiDelete<T>(endpoint: string, workspaceId?: string): Promise<T> {
  const options: ApiRequestOptions = { method: "DELETE" };
  if (workspaceId !== undefined) {
    options.workspaceId = workspaceId;
  }
  return apiRequest<T>(endpoint, options);
}

/**
 * POST request helper for FormData uploads
 * Note: This does not set Content-Type header to allow browser to set multipart/form-data boundary
 */
export async function apiPostFormData<T>(
  endpoint: string,
  formData: FormData,
  workspaceId?: string,
  timeoutMs?: number
): Promise<T> {
  const url = `${API_BASE_URL}${endpoint}`;
  const headers: Record<string, string> = {};

  // Set up abort controller for timeout
  const timeout = timeoutMs ?? DEFAULT_API_TIMEOUT_MS;
  const controller = new AbortController();
  let timeoutId: ReturnType<typeof setTimeout> | undefined;

  if (timeout > 0) {
    timeoutId = setTimeout(() => {
      controller.abort();
    }, timeout);
  }

  try {
    // Add workspace ID header if provided
    if (workspaceId) {
      headers["X-Workspace-Id"] = workspaceId;
    }

    // Add CSRF token for state-changing request
    const token = await ensureCsrfToken();
    headers["X-CSRF-Token"] = token;

    const response = await fetch(url, {
      method: "POST",
      headers,
      body: formData,
      credentials: "include",
      signal: controller.signal,
    });

    if (!response.ok) {
      const error: ApiError = await response.json().catch(
        (): ApiError => ({
          code: "UNKNOWN_ERROR",
          message: response.statusText || "An unknown error occurred",
        })
      );

      // Handle CSRF token mismatch - refresh token and retry once
      if (
        response.status === 403 &&
        (error.code === "CSRF_ERROR" || error.message.includes("CSRF"))
      ) {
        // Refresh CSRF token
        await fetchCsrfToken();

        // Retry the request with new token (recursive call)
        return await apiPostFormData<T>(endpoint, formData, workspaceId, timeoutMs);
      }

      throw new Error(error.message);
    }

    return await (response.json() as Promise<T>);
  } catch (err: unknown) {
    if (err instanceof DOMException && err.name === "AbortError") {
      throw new Error(`Request to ${endpoint} timed out after ${String(timeout)}ms`);
    }
    throw err;
  } finally {
    if (timeoutId !== undefined) {
      clearTimeout(timeoutId);
    }
  }
}
