import smartcar from "smartcar";
import { smartcarClient } from "../config/smartcar";
import SmartcarAccountModel from "../models/SmartcarAccountModel";
import VehicleModel from "../models/vehicleModel";
import UserModel from "../models/UserModel";
import { Types } from "mongoose";
import { resolveSmartcarAccessExpiration } from "../utils/smartcarDate";

const TOKEN_REFRESH_BUFFER_MS = 5 * 60 * 1000;

/**
 * Per-user **API access token** (vehicle data) — store, refresh, Bearer usage:
 * @see https://smartcar.com/docs/getting-started/how-to/manage-api-tokens
 *
 * Application **Management** tokens (webhook VERIFY, subscriptions) are separate — do not use for Vehicle API.
 */

export function isSmartcarAccessTokenUnauthorized(err: unknown): boolean {
  if (!err || typeof err !== "object") return false;
  const e = err as { statusCode?: number };
  return e.statusCode === 401;
}

type SmartcarAccountDoc = InstanceType<typeof SmartcarAccountModel>;

/**
 * Run `fn` with a valid access token. If Smartcar returns **401** (expired / invalid bearer),
 * refreshes with the stored refresh token once and retries, per Smartcar guidance.
 */
export async function withSmartcarAccessToken<T>(
  userId: string,
  fn: (accessToken: string, account: SmartcarAccountDoc) => Promise<T>,
): Promise<T | null> {
  const first = await getValidSmartcarToken(userId);
  if (!first) return null;
  try {
    return await fn(first.accessToken, first.account);
  } catch (e) {
    if (!isSmartcarAccessTokenUnauthorized(e)) throw e;
    const second = await getValidSmartcarToken(userId, { forceRefresh: true });
    if (!second) throw e;
    return await fn(second.accessToken, second.account);
  }
}

/**
 * Get a valid Smartcar access token for a user, refreshing automatically if expired
 * (or immediately when `forceRefresh` — e.g. after a 401 from the Vehicle API).
 * Returns null if no Smartcar account is linked.
 */
export async function getValidSmartcarToken(
  userId: string,
  opts?: { forceRefresh?: boolean },
): Promise<{ accessToken: string; account: SmartcarAccountDoc } | null> {
  const account = await SmartcarAccountModel.findOne({
    userId: new Types.ObjectId(userId),
    status: true,
  });
  if (!account) return null;

  const now = Date.now();
  const expiresAt = account.tokenExpiresAt
    ? new Date(account.tokenExpiresAt).getTime()
    : 0;

  const forceRefresh = !!opts?.forceRefresh;
  if (!forceRefresh && expiresAt > now + TOKEN_REFRESH_BUFFER_MS) {
    return { accessToken: account.accessToken, account };
  }

  try {
    // Smartcar SDK v9 returns { accessToken, refreshToken, expiration: Date,
    // refreshExpiration: Date }. We accept the legacy `expiresIn` shape too
    // for forward/backward compatibility — see resolveSmartcarAccessExpiration.
    const refreshed = (await smartcarClient.exchangeRefreshToken(
      account.refreshToken,
    )) as {
      accessToken: string;
      refreshToken: string;
      expiration?: Date;
      refreshExpiration?: Date;
      expiresIn?: number;
    };

    const tokenExpiresAt = resolveSmartcarAccessExpiration(refreshed);
    if (!tokenExpiresAt) {
      console.error(
        "[Smartcar] exchangeRefreshToken returned an unexpected shape:",
        Object.keys((refreshed ?? {}) as Record<string, unknown>),
      );
      return null;
    }

    account.accessToken = refreshed.accessToken;
    account.refreshToken = refreshed.refreshToken;
    account.tokenExpiresAt = tokenExpiresAt;
    await account.save();

    const secondsUntilExpiry = Math.max(
      0,
      Math.round((tokenExpiresAt.getTime() - Date.now()) / 1000),
    );
    console.log(
      `[Smartcar] Refreshed token for userId=${userId}, expires in ${secondsUntilExpiry}s`,
    );
    return { accessToken: refreshed.accessToken, account };
  } catch (e) {
    const err = e as { type?: string; description?: string; message?: string };
    console.error(
      "[Smartcar] Token refresh failed:",
      err.description || err.message || e,
    );

    if (err.type === "invalid_grant") {
      account.status = false;
      await account.save();
      console.warn(
        `[Smartcar] Disabled account for userId=${userId} — refresh token expired. User must re-link.`,
      );
    }
    return null;
  }
}

/**
 * Sync vehicles from Smartcar into the DB for a given user.
 * Uses the stored token (refreshes if needed). Returns the synced vehicles.
 */
export async function syncSmartcarVehicles(
  userId: string,
  accessToken: string,
  smartcarUserId: string,
  iamScUserId?: string,
): Promise<Array<Record<string, unknown>>> {
  let vehicleIds: string[];
  if (iamScUserId) {
    const connRes = await fetch(
      `https://vehicle.api.smartcar.com/v3/connections?filter[user_id]=${encodeURIComponent(iamScUserId)}`,
      {
        headers: {
          Authorization: `Bearer ${accessToken}`,
          "sc-user-id": iamScUserId,
        },
      },
    );
    if (connRes.ok) {
      const data = (await connRes.json()) as {
        data?: Array<{
          relationships?: {
            vehicle?: { data?: { id?: string } };
          };
        }>;
      };
      vehicleIds = (data.data ?? [])
        .map((c) => c.relationships?.vehicle?.data?.id)
        .filter(Boolean) as string[];
    } else {
      console.warn(
        `[Smartcar] IAM Connections API failed (${connRes.status}) — falling back to SDK getVehicles`,
      );
      const result = await smartcar.getVehicles(accessToken);
      vehicleIds = result.vehicles;
    }
  } else {
    const result = await smartcar.getVehicles(accessToken);
    vehicleIds = result.vehicles;
  }
  const results: Array<Record<string, unknown>> = [];

  await Promise.all(
    vehicleIds.map(async (vehicleId: string) => {
      try {
        let make = "";
        let model = "";
        let year = 0;
        let smartcarVehicleIdResolved = vehicleId;
        let odometerKm: number | null = null;
        let fuelPercent: number | null = null;
        let fuelAmountLiters: number | null = null;

        if (iamScUserId) {
          const iamHeaders: Record<string, string> = {
            Authorization: `Bearer ${accessToken}`,
            "sc-user-id": iamScUserId,
          };
          const vRes = await fetch(
            `https://vehicle.api.smartcar.com/v3/vehicles/${vehicleId}`,
            { headers: iamHeaders },
          );
          if (vRes.ok) {
            const vData = (await vRes.json()) as {
              data?: {
                id?: string;
                attributes?: {
                  make?: string;
                  model?: string;
                  year?: number;
                };
              };
            };
            const attrs = vData.data?.attributes;
            make = attrs?.make ?? "";
            model = attrs?.model ?? "";
            year = attrs?.year ?? 0;
            smartcarVehicleIdResolved = vData.data?.id ?? vehicleId;
          } else {
            console.warn(
              `[Smartcar] v3 vehicle info failed for ${vehicleId}: ${vRes.status}`,
            );
          }
        } else {
          const vehicle = new smartcar.Vehicle(vehicleId, accessToken);
          const attributes = await vehicle.attributes();
          make = attributes.make;
          model = attributes.model;
          year = attributes.year;
          smartcarVehicleIdResolved = attributes.id;

          try {
            const batch = await vehicle.batch(["/odometer", "/fuel"]);
            const odometerData = batch.odometer();
            const fuelData = batch.fuel();
            odometerKm = odometerData?.distance ?? null;
            fuelPercent = fuelData?.percentRemaining ?? null;
            fuelAmountLiters = fuelData?.amountRemaining ?? null;
          } catch {
            // Some vehicles don't support all endpoints
          }
        }

        const odometerMiles =
          odometerKm != null ? Math.round(odometerKm * 0.621371) : null;
        const fuelGallons =
          fuelAmountLiters != null
            ? Math.round(fuelAmountLiters * 0.264172 * 100) / 100
            : null;

        const doc = await VehicleModel.findOneAndUpdate(
          {
            smartcarVehicleId: smartcarVehicleIdResolved,
            userId: new Types.ObjectId(userId),
          },
          {
            name: `${make} ${model}`.trim() || "Unknown Vehicle",
            vehicleModel: model,
            manufacturingYear: String(year || ""),
            tankCapacity:
              fuelPercent != null ? `${Math.round(fuelPercent * 100)}%` : "",
            currentMPG: fuelGallons != null ? String(fuelGallons) : "",
            vehicleId: vehicleId,
            fuelType: "",
            odoMeter: odometerMiles != null ? String(odometerMiles) : "",
            registrationNumber: "",
            smartcarVehicleId: smartcarVehicleIdResolved,
            smartcarUserId,
            userId: new Types.ObjectId(userId),
            isDefault: false,
            isDeleted: false,
          },
          { upsert: true, new: true },
        );

        results.push({
          vehicleId: doc._id,
          smartcarVehicleId: smartcarVehicleIdResolved,
          name: `${make} ${model}`.trim() || "Unknown Vehicle",
          year,
          make,
          model,
          odometerMiles,
          fuelPercent:
            fuelPercent != null ? Math.round(fuelPercent * 100) : null,
          fuelGallons,
        });
      } catch (err: unknown) {
        const e = err as { type?: string; message?: string };
        if (e.type === "PERMISSION") {
          console.warn(
            `[Smartcar] Insufficient permissions for vehicle ${vehicleId} — user needs to re-authorize`,
          );
        } else {
          console.error(
            `[Smartcar] Failed to process vehicle ${vehicleId}:`,
            e.message || err,
          );
        }
      }
    }),
  );

  return results;
}

/**
 * After pulling vehicles from Smartcar: stamp sync time on the Smartcar account (Connect OAuth row)
 * and set `User.isCarsRegistered` from whether this user has any vehicle documents (Smartcar or manual).
 */
export async function afterSmartcarVehicleSync(
  tankTrackUserId: string,
  smartcarUserId: string,
  syncSucceeded: boolean,
): Promise<void> {
  if (syncSucceeded) {
    await SmartcarAccountModel.updateOne(
      { smartcarUserId },
      { $set: { lastVehicleSyncAt: new Date() } },
    );
  }
  const vehicleCount = await VehicleModel.countDocuments({
    userId: new Types.ObjectId(tankTrackUserId),
    isDeleted: false,
  });

  const hasSmartcarAccount = await SmartcarAccountModel.exists({
    userId: new Types.ObjectId(tankTrackUserId),
    status: true,
  });

  const isCarsRegistered = vehicleCount > 0 || Boolean(hasSmartcarAccount);

  console.log(
    "[Smartcar][DEBUG] afterSmartcarVehicleSync — user=%s vehicleCount=%d hasSmartcarAccount=%s → isCarsRegistered=%s",
    tankTrackUserId.slice(0, 8) + "…",
    vehicleCount,
    Boolean(hasSmartcarAccount),
    isCarsRegistered,
  );

  await UserModel.findByIdAndUpdate(tankTrackUserId, {
    $set: {
      isCarsRegistered,
      smartcarToken: smartcarUserId,
    },
  });
}
