import axios, { type AxiosRequestConfig } from "axios";
import {
  handleApiError,
  statsApi,
  getRemoteStatsApi,
  isElectron,
  sshHostApi,
} from "@/main-axios";
import type { ServerMetrics, ServerStatus } from "@/main-axios";
import { getCachedServerStatuses } from "@/lib/hosts-request-cache";
import { resolveConnectionOrigin } from "@/lib/connection-origin";
import type { SSHHost } from "@/types/index";
import { createKeyedRequestCache } from "@/lib/keyed-request-cache";

// Metrics collection/viewer registration below (startMetricsPolling,
// registerMetricsViewer, etc.) is NOT origin-routed: the backend that
// receives the call must own the target host by numeric database id, and a
// synced host has a different numeric id in each database (only its
// syncId matches across them). Only the aggregate status read is merged
// across local + remote, same as tunnel status.
async function isRemoteSyncConnected(): Promise<boolean> {
  if (!isElectron()) return false;
  try {
    const config = (await window.electronAPI?.invoke?.(
      "get-remote-sync-config",
    )) as { serverUrl?: string } | null;
    return !!config?.serverUrl;
  } catch {
    return false;
  }
}

type ApiConnectionLog = {
  type: "info" | "success" | "warning" | "error";
  stage: string;
  message: string;
  details?: Record<string, unknown>;
};

type ConnectErrorResponse = {
  error?: string;
  message?: string;
  connectionLogs?: ApiConnectionLog[];
  requires_totp?: boolean;
  requires_warpgate?: boolean;
  sessionId?: string;
  prompt?: string;
  url?: string;
  securityKey?: string;
  status?: string;
  reason?: string;
};

// SERVER STATISTICS
// ============================================================================

const metricsCache = createKeyedRequestCache<ServerMetrics | null>(1_500, 100);

/**
 * Progressive retry schedule for the background /status poll.
 *
 * Each entry describes one attempt's per-request timeout and the pause to
 * observe before the next attempt. The pause on the last entry is `null`:
 * after that final failure we surface the network error, which flows
 * through the response interceptor + dbHealthMonitor (which decides
 * between the degraded toast and the full-outage overlay based on whether
 * any WebSocket is still alive).
 *
 * Sequence: try(2s) -> wait 3s -> try(5s) -> wait 5s -> try(8s) -> fail.
 * Worst-case wall-clock = 23s, which fits inside the 30s ServerStatusContext
 * poll cadence, so the next tick acts as the next retry without overlap.
 */
const STATUS_RETRY_SCHEDULE: ReadonlyArray<{
  timeoutMs: number;
  pauseAfterMs: number | null;
}> = [
  { timeoutMs: 2000, pauseAfterMs: 3000 },
  { timeoutMs: 5000, pauseAfterMs: 5000 },
  { timeoutMs: 8000, pauseAfterMs: null },
];

function isTransientStatusError(error: unknown): boolean {
  if (!axios.isAxiosError(error)) return false;
  if (error.response) {
    // Definitive server response (even 5xx) is not something more retries
    // will fix in a useful timeframe; bail out and report it normally.
    return false;
  }
  const code = error.code;
  if (!code) {
    // No code + no response means classic network error (offline / DNS / TCP)
    return true;
  }
  return (
    code === "ECONNABORTED" ||
    code === "ETIMEDOUT" ||
    code === "ERR_NETWORK" ||
    code === "ECONNREFUSED" ||
    code === "ECONNRESET"
  );
}

export async function getAllServerStatuses(): Promise<
  Record<number, ServerStatus>
> {
  return getCachedServerStatuses(async () => {
    let lastError: unknown = null;
    let localStatuses: Record<number, ServerStatus> = {};
    let localHostIds: number[] | null = null;

    if (isElectron()) {
      try {
        const response = await sshHostApi.get<SSHHost[]>("/db/host");
        const defaultOrigin = await resolveConnectionOrigin({
          connectionType: "ssh",
          connectionOrigin: null,
        });
        localHostIds = (response.data || [])
          .filter(
            (host) => (host.connectionOrigin ?? defaultOrigin) === "local",
          )
          .map((host) => host.id);
      } catch {
        // A host-list failure must not start local probes for hosts whose
        // configured origin may be remote.
        localHostIds = [];
      }
    }

    for (let i = 0; i < STATUS_RETRY_SCHEDULE.length; i++) {
      const { timeoutMs, pauseAfterMs } = STATUS_RETRY_SCHEDULE[i];
      const isFinalAttempt = i === STATUS_RETRY_SCHEDULE.length - 1;

      try {
        const response = await statsApi.get("/status", {
          timeout: timeoutMs,
          ...(localHostIds === null
            ? {}
            : { params: { hostIds: localHostIds.join(",") } }),
          // Silence per-attempt interceptor logging & health-monitor side
          // effects on all attempts except the final one, so background
          // blips don't look like real outages.
          __silentRetry: !isFinalAttempt,
        } as AxiosRequestConfig & { __silentRetry?: boolean });
        localStatuses = response.data || {};
        lastError = null;
        break;
      } catch (error) {
        lastError = error;
        if (!isTransientStatusError(error)) {
          break;
        }
        if (pauseAfterMs === null) {
          break;
        }
        await new Promise((resolve) => setTimeout(resolve, pauseAfterMs));
      }
    }

    if (lastError) {
      handleApiError(lastError, "fetch server statuses");
      return {};
    }

    if (await isRemoteSyncConnected()) {
      try {
        const remoteResult = await getRemoteStatsApi().get("/status", {
          timeout: 8000,
          __silentRetry: true,
        } as AxiosRequestConfig & { __silentRetry?: boolean });
        return { ...localStatuses, ...(remoteResult.data || {}) };
      } catch {
        // remote unreachable this tick -- fall back to local-only statuses
      }
    }

    return localStatuses;
  });
}

export async function getServerStatusById(id: number): Promise<ServerStatus> {
  try {
    const response = await statsApi.get(`/status/${id}`);
    return response.data;
  } catch (error) {
    handleApiError(error, "fetch server status");
    throw error;
  }
}

export async function getServerMetricsById(
  id: number,
): Promise<ServerMetrics | null> {
  return metricsCache.get(String(id), async () => {
    try {
      const response = await statsApi.get(`/metrics/${id}`, {
        // Treat 404 as an expected "no metrics yet / disabled" signal rather
        // than an error so we don't spam warn logs on the client.
        validateStatus: (status) => status === 200 || status === 404,
      });
      if (response.status === 404) return null;
      return response.data;
    } catch (error) {
      // If a 404 still slips through (e.g. intercepted before reaching here),
      // swallow it quietly; everything else still flows through handleApiError.
      if (axios.isAxiosError(error) && error.response?.status === 404) {
        return null;
      }
      handleApiError(error, "fetch server metrics");
    }
  });
}

export async function startMetricsPolling(hostId: number): Promise<{
  success: boolean;
  requires_totp?: boolean;
  sessionId?: string;
  prompt?: string;
  viewerSessionId?: string;
  connectionLogs?: ApiConnectionLog[];
}> {
  try {
    const response = await statsApi.post(`/metrics/start/${hostId}`);
    metricsCache.invalidate(String(hostId));
    return response.data;
  } catch (error: unknown) {
    if (
      axios.isAxiosError<ConnectErrorResponse>(error) &&
      error.response?.data?.connectionLogs
    ) {
      const data = error.response.data;
      const errorWithLogs = new Error(
        data.error || data.message || error.message,
      );
      Object.assign(errorWithLogs, {
        connectionLogs: data.connectionLogs,
      });
      throw errorWithLogs;
    }
    handleApiError(error, "start metrics polling");
    throw error;
  }
}

export async function stopMetricsPolling(
  hostId: number,
  viewerSessionId?: string,
): Promise<void> {
  try {
    await statsApi.post(`/metrics/stop/${hostId}`, { viewerSessionId });
  } catch (error) {
    handleApiError(error, "stop metrics polling");
    throw error;
  }
}

export async function sendMetricsHeartbeat(
  viewerSessionId: string,
): Promise<void> {
  try {
    await statsApi.post("/metrics/heartbeat", { viewerSessionId });
  } catch (error) {
    handleApiError(error, "send metrics heartbeat");
    throw error;
  }
}

export async function registerMetricsViewer(hostId: number): Promise<{
  success: boolean;
  viewerSessionId?: string;
  skipped?: boolean;
  reason?: string;
}> {
  try {
    const response = await statsApi.post("/metrics/register-viewer", {
      hostId,
    });
    metricsCache.invalidate(String(hostId));
    return response.data;
  } catch (error) {
    handleApiError(error, "register metrics viewer");
    throw error;
  }
}

export async function unregisterMetricsViewer(
  hostId: number,
  viewerSessionId: string,
): Promise<void> {
  try {
    await statsApi.post("/metrics/unregister-viewer", {
      hostId,
      viewerSessionId,
    });
  } catch (error) {
    handleApiError(error, "unregister metrics viewer");
    throw error;
  }
}

export async function submitMetricsTOTP(
  sessionId: string,
  totpCode: string,
): Promise<{
  success: boolean;
  viewerSessionId?: string;
}> {
  try {
    const response = await statsApi.post("/metrics/connect-totp", {
      sessionId,
      totpCode,
    });
    metricsCache.invalidate();
    return response.data;
  } catch (error) {
    handleApiError(error, "submit metrics TOTP");
    throw error;
  }
}

export async function refreshServerPolling(): Promise<void> {
  try {
    await statsApi.post("/refresh");
  } catch (error) {
    console.warn("Failed to refresh server polling:", error);
  }
}

export async function notifyHostCreatedOrUpdated(
  hostId: number,
): Promise<void> {
  try {
    await statsApi.post("/host-updated", { hostId });
    metricsCache.invalidate(String(hostId));
  } catch (error) {
    console.warn("Failed to notify stats server of host update:", error);
  }
}

// ============================================================================
