import smartcar from "smartcar";
import { Types } from "mongoose";
import {
  smartcarClient,
  smartcarFallbackClient,
  SMARTCAR_CONFIGURED,
  SMARTCAR_EFFECTIVE_REDIRECT_URI,
} from "../config/smartcar";
import SmartcarAccountModel from "../models/SmartcarAccountModel";
import { resolveSmartcarAccessExpiration } from "../utils/smartcarDate";
import {
  afterSmartcarVehicleSync,
  syncSmartcarVehicles,
} from "./smartcarService";
import { subscribeVehiclesToDefaultWebhook } from "./smartcarWebhookSubscription";
import { resolveSmartcarIamTokenUrl } from "./smartcarIamApplicationToken";

export type LinkSmartcarWithCodeResult =
  | {
      ok: true;
      smartcarUserId: string;
      /** Vehicles returned by the post-link Smartcar sync (Tank `Vehicle` docs updated). */
      vehicles: Array<Record<string, unknown>>;
      vehicleSyncCompleted: boolean;
    }
  | {
      ok: false;
      detail: string;
      invalidGrant?: boolean;
      invalidClient?: boolean;
    };

/**
 * Exchange Smartcar OAuth `code` and link the resulting account to `tankTrackUserId`.
 * Used by GET `/smartcar/redirect` and POST `/smartcar/complete-connect` (JWT fallback when `state` is dropped).
 */
export type ExchangeCodeOnlyResult =
  | {
      ok: true;
      smartcarUserId: string;
      accessToken: string;
      refreshToken: string;
      tokenExpiresAt: Date;
    }
  | {
      ok: false;
      detail: string;
      invalidGrant?: boolean;
      invalidClient?: boolean;
    };

type SmartcarAccessTokens = {
  accessToken: string;
  refreshToken: string;
  expiration: Date;
  refreshExpiration: Date;
};

function isInvalidClientError(err: unknown): boolean {
  const e = err as { type?: string; description?: string; message?: string };
  return (
    e.type === "invalid_client" ||
    /invalid_client|incorrect client credentials/i.test(
      String(e.description || e.message || ""),
    )
  );
}

/**
 * Get an app-level access token via IAM client_credentials flow.
 * Uses SMARTCAR_IAM_CLIENT_ID + SMARTCAR_CLIENT_SECRET at SMARTCAR_IAM_TOKEN_URL (default iam.smartcar.com).
 * @see https://smartcar.com/docs/getting-started/how-to/api-authentication
 */
async function getIamApplicationToken(): Promise<{
  accessToken: string;
  expiresIn: number;
} | null> {
  const iamClientId = process.env.SMARTCAR_IAM_CLIENT_ID?.trim();
  const clientSecret = process.env.SMARTCAR_CLIENT_SECRET?.trim();
  if (!iamClientId || !clientSecret) {
    console.error(
      "[Smartcar][DEBUG] getIamApplicationToken — MISSING env: IAM_CLIENT_ID=%s SECRET=%s",
      iamClientId ? "set" : "MISSING",
      clientSecret ? "set" : "MISSING",
    );
    return null;
  }

  console.log(
    "[Smartcar][DEBUG] getIamApplicationToken — requesting IAM token with client_id=%s…",
    iamClientId.slice(0, 12) + "…",
  );

  const body = new URLSearchParams({
    grant_type: "client_credentials",
    client_id: iamClientId,
    client_secret: clientSecret,
  }).toString();

  const res = await fetch(resolveSmartcarIamTokenUrl(), {
    method: "POST",
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
    },
    body,
  });

  if (!res.ok) {
    const text = await res.text();
    console.error(
      "[Smartcar][DEBUG] getIamApplicationToken — FAILED status=%d body=%s",
      res.status,
      text.slice(0, 300),
    );
    return null;
  }

  const data = (await res.json()) as {
    access_token: string;
    expires_in: number;
  };
  console.log(
    "[Smartcar][DEBUG] getIamApplicationToken — SUCCESS expires_in=%ds",
    data.expires_in,
  );
  return { accessToken: data.access_token, expiresIn: data.expires_in };
}

async function exchangeCodeWithFallback(
  code: string,
): Promise<SmartcarAccessTokens> {
  console.log(
    "[Smartcar][DEBUG] STEP-1 exchangeCodeWithFallback — trying PRIMARY SDK client…",
  );
  try {
    const result = (await smartcarClient.exchangeCode(
      code,
    )) as SmartcarAccessTokens;
    console.log(
      "[Smartcar][DEBUG] STEP-1 PRIMARY SDK exchange — SUCCESS (has refreshToken=%s)",
      Boolean(result.refreshToken),
    );
    return result;
  } catch (err: unknown) {
    const e = err as { type?: string; message?: string };
    console.error(
      "[Smartcar][DEBUG] STEP-1 PRIMARY SDK exchange — FAILED type=%s msg=%s",
      e.type ?? "unknown",
      (e.message ?? "").slice(0, 150),
    );

    if (isInvalidClientError(err) && smartcarFallbackClient) {
      console.log(
        "[Smartcar][DEBUG] STEP-2 trying FALLBACK SDK client (IAM client_id)…",
      );
      try {
        const result = (await smartcarFallbackClient.exchangeCode(
          code,
        )) as SmartcarAccessTokens;
        console.log("[Smartcar][DEBUG] STEP-2 FALLBACK SDK exchange — SUCCESS");
        return result;
      } catch (err2: unknown) {
        const e2 = err2 as { type?: string; message?: string };
        console.error(
          "[Smartcar][DEBUG] STEP-2 FALLBACK SDK exchange — FAILED type=%s msg=%s",
          e2.type ?? "unknown",
          (e2.message ?? "").slice(0, 150),
        );

        if (isInvalidClientError(err2)) {
          console.log(
            "[Smartcar][DEBUG] STEP-3 both SDK clients failed — trying IAM client_credentials…",
          );
          const iamToken = await getIamApplicationToken();
          if (iamToken) {
            console.log(
              "[Smartcar][DEBUG] STEP-3 IAM client_credentials — SUCCESS",
            );
            return {
              accessToken: iamToken.accessToken,
              refreshToken: "",
              expiration: new Date(Date.now() + iamToken.expiresIn * 1000),
              refreshExpiration: new Date(
                Date.now() + iamToken.expiresIn * 1000,
              ),
            };
          }
          console.error(
            "[Smartcar][DEBUG] STEP-3 IAM client_credentials — ALSO FAILED",
          );
        }
        throw err2;
      }
    }
    throw err;
  }
}

/**
 * Exchange the OAuth code with Smartcar WITHOUT needing a Tank Track user ID.
 * Tries SDK code exchange first; falls back to IAM client_credentials if invalid_client.
 * @param code  - The OAuth authorization code from Connect redirect.
 * @param smartcarUserIdHint - The `user_id` from Connect redirect URL (new IAM API provides this).
 */
export async function exchangeSmartcarCodeOnly(
  code: string,
  smartcarUserIdHint?: string,
): Promise<ExchangeCodeOnlyResult> {
  console.log(
    "[Smartcar][DEBUG] exchangeSmartcarCodeOnly — code=%s… userIdHint=%s",
    code.slice(0, 12),
    smartcarUserIdHint ? smartcarUserIdHint.slice(0, 8) + "…" : "(none)",
  );

  if (!SMARTCAR_CONFIGURED || !SMARTCAR_EFFECTIVE_REDIRECT_URI) {
    console.error(
      "[Smartcar][DEBUG] exchangeSmartcarCodeOnly — NOT CONFIGURED configured=%s redirectUri=%s",
      SMARTCAR_CONFIGURED,
      SMARTCAR_EFFECTIVE_REDIRECT_URI ?? "null",
    );
    return {
      ok: false,
      detail: "Smartcar OAuth is not configured on this server.",
    };
  }
  try {
    const access = await exchangeCodeWithFallback(code);
    console.log(
      "[Smartcar][DEBUG] exchangeSmartcarCodeOnly — code exchange done, resolving expiration…",
    );
    const tokenExpiresAt = resolveSmartcarAccessExpiration(access);
    if (!tokenExpiresAt) {
      console.error(
        "[Smartcar][DEBUG] exchangeSmartcarCodeOnly — INVALID token expiration",
      );
      return {
        ok: false,
        detail: "Smartcar returned an invalid token expiration.",
      };
    }

    let userId = smartcarUserIdHint || "";
    if (!userId && access.refreshToken) {
      console.log(
        "[Smartcar][DEBUG] exchangeSmartcarCodeOnly — fetching user via smartcar.getUser…",
      );
      const smartcarUser = await smartcar.getUser(access.accessToken);
      userId = smartcarUser.id;
      console.log(
        "[Smartcar][DEBUG] exchangeSmartcarCodeOnly — getUser OK userId=%s…",
        userId.slice(0, 8),
      );
    }

    console.log(
      "[Smartcar][DEBUG] exchangeSmartcarCodeOnly — SUCCESS userId=%s tokenExpires=%s",
      userId ? userId.slice(0, 8) + "…" : "(empty)",
      tokenExpiresAt.toISOString(),
    );

    return {
      ok: true,
      smartcarUserId: userId,
      accessToken: access.accessToken,
      refreshToken: access.refreshToken,
      tokenExpiresAt,
    };
  } catch (err: unknown) {
    const smartcarErr = err as {
      description?: string;
      message?: string;
      type?: string;
    };
    const msg =
      smartcarErr.description || smartcarErr.message || "Smartcar error";
    if (smartcarErr.type === "invalid_grant") {
      return {
        ok: false,
        detail: "Invalid or expired authorization code.",
        invalidGrant: true,
      };
    }
    if (isInvalidClientError(err)) {
      console.warn(
        "[Smartcar] All SDK code-exchange attempts failed — trying direct IAM client_credentials…",
      );
      const iamToken = await getIamApplicationToken();
      if (iamToken) {
        const expiresAt = new Date(Date.now() + iamToken.expiresIn * 1000);
        const userId = smartcarUserIdHint || "";
        console.info(
          `[Smartcar] IAM client_credentials token obtained ✓ user_id=${userId ? userId.slice(0, 8) + "…" : "(none)"}`,
        );
        return {
          ok: true,
          smartcarUserId: userId,
          accessToken: iamToken.accessToken,
          refreshToken: "",
          tokenExpiresAt: expiresAt,
        };
      }
      return {
        ok: false,
        detail: "Smartcar rejected client credentials (both SDK and IAM).",
        invalidClient: true,
      };
    }
    return { ok: false, detail: msg };
  }
}

export async function linkSmartcarWithAuthorizationCode(opts: {
  tankTrackUserId: string;
  code: string;
  smartcarUserIdQuery?: string;
}): Promise<LinkSmartcarWithCodeResult> {
  console.log(
    "[Smartcar][DEBUG] linkSmartcarWithAuthorizationCode — tankUser=%s code=%s… queryUserId=%s",
    opts.tankTrackUserId.slice(0, 8) + "…",
    opts.code.slice(0, 12),
    opts.smartcarUserIdQuery
      ? opts.smartcarUserIdQuery.slice(0, 8) + "…"
      : "(none)",
  );

  if (!SMARTCAR_CONFIGURED || !SMARTCAR_EFFECTIVE_REDIRECT_URI) {
    console.error("[Smartcar][DEBUG] linkSmartcar — NOT CONFIGURED");
    return {
      ok: false,
      detail:
        "Smartcar OAuth is not configured on this server. Set SMARTCAR_CLIENT_ID and SMARTCAR_REDIRECT_URI (or PUBLIC_API_URL + API_PREFIX).",
    };
  }

  try {
    let accessToken: string;
    let refreshToken: string;
    let tokenExpiresAt: Date;
    let smartcarUserId: string;
    let usedIam = false;

    try {
      console.log("[Smartcar][DEBUG] linkSmartcar — exchanging code…");
      const access = await exchangeCodeWithFallback(opts.code);
      const expiry = resolveSmartcarAccessExpiration(access);
      if (!expiry) {
        return {
          ok: false,
          detail:
            "Smartcar returned an invalid token expiration. Please try linking again.",
        };
      }
      tokenExpiresAt = expiry;
      accessToken = access.accessToken;
      refreshToken = access.refreshToken;

      if (access.refreshToken) {
        const smartcarUser = await smartcar.getUser(access.accessToken);
        smartcarUserId = smartcarUser.id;
        if (
          opts.smartcarUserIdQuery &&
          opts.smartcarUserIdQuery.toLowerCase() !==
            smartcarUser.id?.toLowerCase()
        ) {
          console.warn(
            `[Smartcar] Redirect query user_id (${opts.smartcarUserIdQuery}) != GET /v2/user id (${smartcarUser.id}).`,
          );
        }
      } else {
        smartcarUserId = opts.smartcarUserIdQuery || "";
        usedIam = true;
      }
    } catch (err: unknown) {
      if (!isInvalidClientError(err)) throw err;

      console.warn(
        "[Smartcar] SDK code exchange failed — falling back to IAM client_credentials…",
      );
      const iamToken = await getIamApplicationToken();
      if (!iamToken) {
        console.error(
          "[Smartcar] IAM client_credentials also failed. Check SMARTCAR_IAM_CLIENT_ID + SMARTCAR_CLIENT_SECRET.",
        );
        return {
          ok: false,
          detail: "Smartcar rejected client credentials (both SDK and IAM).",
          invalidClient: true,
        };
      }
      accessToken = iamToken.accessToken;
      refreshToken = "";
      tokenExpiresAt = new Date(Date.now() + iamToken.expiresIn * 1000);
      smartcarUserId = opts.smartcarUserIdQuery || "";
      usedIam = true;
      console.info(
        `[Smartcar] IAM token obtained ✓ user_id=${smartcarUserId ? smartcarUserId.slice(0, 8) + "…" : "(none)"}`,
      );
    }

    console.log(
      "[Smartcar][DEBUG] linkSmartcar — saving SmartcarAccount to DB smartcarUserId=%s tankUser=%s usedIam=%s",
      smartcarUserId ? smartcarUserId.slice(0, 8) + "…" : "(empty)",
      opts.tankTrackUserId.slice(0, 8) + "…",
      usedIam,
    );

    await SmartcarAccountModel.findOneAndUpdate(
      { smartcarUserId },
      {
        smartcarUserId,
        accessToken,
        refreshToken,
        tokenExpiresAt,
        status: true,
        userId: new Types.ObjectId(opts.tankTrackUserId),
      },
      { upsert: true, new: true },
    );
    console.log("[Smartcar][DEBUG] linkSmartcar — SmartcarAccount saved ✓");

    let syncedVehicles: Array<Record<string, unknown>> = [];
    let vehicleSyncCompleted = false;
    try {
      console.log(
        "[Smartcar][DEBUG] linkSmartcar — syncing vehicles (iam=%s)…",
        usedIam,
      );
      syncedVehicles = await syncSmartcarVehicles(
        opts.tankTrackUserId,
        accessToken,
        smartcarUserId,
        usedIam ? smartcarUserId : undefined,
      );
      vehicleSyncCompleted = true;
      console.log(
        "[Smartcar][DEBUG] linkSmartcar — vehicles synced ✓ count=%d",
        syncedVehicles.length,
      );

      if (process.env.SMARTCAR_AUTO_SUBSCRIBE_WEBHOOK?.trim() === "true") {
        void subscribeVehiclesToDefaultWebhook({
          smartcarUserId,
          vehicles: syncedVehicles,
        }).catch((subErr) =>
          console.error(
            "[Smartcar] Auto webhook subscribe failed (non-fatal):",
            subErr,
          ),
        );
      }
    } catch (syncErr) {
      console.error(
        "[Smartcar][DEBUG] linkSmartcar — vehicle sync FAILED (non-fatal):",
        syncErr,
      );
    }

    console.log(
      "[Smartcar][DEBUG] linkSmartcar — calling afterSmartcarVehicleSync (sets isCarsRegistered)…",
    );
    await afterSmartcarVehicleSync(
      opts.tankTrackUserId,
      smartcarUserId,
      vehicleSyncCompleted,
    );
    console.log(
      "[Smartcar][DEBUG] linkSmartcar — COMPLETE ✓ smartcarUserId=%s vehicles=%d isCarsRegistered=true",
      smartcarUserId ? smartcarUserId.slice(0, 8) + "…" : "(empty)",
      syncedVehicles.length,
    );

    return {
      ok: true,
      smartcarUserId,
      vehicles: syncedVehicles,
      vehicleSyncCompleted,
    };
  } catch (err: unknown) {
    const smartcarErr = err as {
      description?: string;
      message?: string;
      type?: string;
    };
    const msg =
      smartcarErr.description || smartcarErr.message || "Smartcar error";
    console.error("[Smartcar] OAuth exchange error:", msg, smartcarErr.type);

    if (smartcarErr.type === "invalid_grant") {
      console.error(
        "[Smartcar] invalid_grant — verify SMARTCAR_REDIRECT_URI matches Developer Portal. Effective:",
        SMARTCAR_EFFECTIVE_REDIRECT_URI,
      );
      return {
        ok: false,
        detail:
          "Invalid or expired authorization code (invalid_grant). Start Smartcar linking again from the app.",
        invalidGrant: true,
      };
    }

    return { ok: false, detail: msg };
  }
}
