import axios from "axios";

/**
 * Smartcar IAM — Application Access Token (OAuth2 client_credentials).
 * @see https://smartcar.com/docs/api-reference/authorization/request-access-token
 * @see https://smartcar.com/docs/getting-started/how-to/api-authentication
 *
 * This is distinct from Smartcar Connect’s authorization **`code`** → user access/refresh tokens
 * (`AuthClient.exchangeCode`). Same Dashboard **API Credentials** (`client_id` / `client_secret`)
 * apply to IAM grants per Smartcar docs.
 *
 * Returned token is **application-level**, ~1 hour, no refresh_token — callers should use this helper’s cache.
 */

export const DEFAULT_SMARTCAR_IAM_TOKEN_URL =
  "https://iam.smartcar.com/oauth2/token";
const BUFFER_MS = 120 * 1000;

type IamSuccess = {
  access_token: string;
  token_type?: string;
  expires_in?: number;
};

type Cached = { bucket: string; token: string; expiresAtMs: number };

let cache: Cached | null = null;
let inflight: Promise<Cached> | null = null;

/** IAM OAuth2 token endpoint; override via `SMARTCAR_IAM_TOKEN_URL`. */
export function resolveSmartcarIamTokenUrl(): string {
  const raw = process.env.SMARTCAR_IAM_TOKEN_URL?.trim();
  return raw && raw.length > 0 ? raw : DEFAULT_SMARTCAR_IAM_TOKEN_URL;
}

export function clearSmartcarIamTokenCache(): void {
  cache = null;
}

/**
 * Obtains an application Bearer token suitable for vehicle.api v3 + `sc-user-id` flows (future / opt-in).
 * Uses in-memory reuse until shortly before IAM `expires_in`.
 */
export async function getCachedSmartcarIamApplicationAccessToken(opts?: {
  clientId?: string;
  clientSecret?: string;
}): Promise<{ accessToken: string; expiresAtMs: number; expiresIn: number }> {
  const iamId = process.env.SMARTCAR_IAM_CLIENT_ID?.trim();
  const clientId =
    opts?.clientId?.trim() ??
    iamId ??
    process.env.SMARTCAR_CLIENT_ID?.trim() ??
    "";
  const clientSecret =
    opts?.clientSecret?.trim() ??
    process.env.SMARTCAR_CLIENT_SECRET?.trim() ??
    "";

  if (!clientId || !clientSecret) {
    throw new Error(
      "Smartcar IAM: set SMARTCAR_IAM_CLIENT_ID (recommended) or SMARTCAR_CLIENT_ID, plus SMARTCAR_CLIENT_SECRET.",
    );
  }

  const now = Date.now();
  /** Cache must not mix IAM `client_*` vs Connect UUID identities. */
  const cacheBucket = clientId;
  if (
    cache &&
    cache.bucket === cacheBucket &&
    cache.expiresAtMs > now + BUFFER_MS
  ) {
    return {
      accessToken: cache.token,
      expiresAtMs: cache.expiresAtMs,
      expiresIn: Math.max(0, Math.round((cache.expiresAtMs - now) / 1000)),
    };
  }

  const runFetch = async (): Promise<Cached> => {
    const body = new URLSearchParams({
      grant_type: "client_credentials",
      client_id: clientId,
      client_secret: clientSecret,
    });

    const res = await axios.post<IamSuccess>(
      resolveSmartcarIamTokenUrl(),
      body.toString(),
      {
        headers: { "Content-Type": "application/x-www-form-urlencoded" },
        validateStatus: () => true,
        timeout: 25_000,
      },
    );

    if (res.status !== 200 || !res.data?.access_token) {
      const errBody =
        res.data &&
        typeof res.data === "object" &&
        "error" in res.data &&
        typeof (res.data as { error?: unknown }).error === "string"
          ? (res.data as { error: string }).error
          : `HTTP ${res.status}`;
      throw new Error(
        `Smartcar IAM token request failed (${errBody}). ` +
          (res.status === 401
            ? "Verify IAM / Connect credentials: SMARTCAR_IAM_CLIENT_ID or SMARTCAR_CLIENT_ID + SMARTCAR_CLIENT_SECRET."
            : ""),
      );
    }

    const expiresIn =
      typeof res.data.expires_in === "number" &&
      Number.isFinite(res.data.expires_in)
        ? res.data.expires_in
        : 3600;

    const issuedAtMs = Date.now();
    const next: Cached = {
      bucket: cacheBucket,
      token: res.data.access_token,
      expiresAtMs: issuedAtMs + expiresIn * 1000,
    };
    cache = next;
    return next;
  };

  /** New client bucket or expired cache → avoid reusing overlapping inflight for different identities. */
  if (!cache || cache.bucket !== cacheBucket) {
    inflight = null;
  }

  if (!inflight) {
    inflight = runFetch().finally(() => {
      inflight = null;
    });
  }

  const c = await inflight;
  return {
    accessToken: c.token,
    expiresAtMs: c.expiresAtMs,
    expiresIn: Math.max(0, Math.round((c.expiresAtMs - Date.now()) / 1000)),
  };
}
