import { Middleware } from "@simple-api/core";
import { useAuthStore } from "./auth-store";
import { toast } from "./toast-store";

/**
 * Decode base64url string (used for JWT payloads)
 * React Native does not have a global Buffer, so we use a simple decoder.
 */
function decodeBase64(str: string) {
  const chars =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
  let out = "";
  str = str.replace(/[-_]/g, (m) => ({ "-": "+", _: "/" })[m] || m);
  while (str.length % 4) str += "=";

  for (let i = 0; i < str.length; i += 4) {
    const a = chars.indexOf(str.charAt(i));
    const b = chars.indexOf(str.charAt(i + 1));
    const c = chars.indexOf(str.charAt(i + 2));
    const d = chars.indexOf(str.charAt(i + 3));

    out += String.fromCharCode((a << 2) | (b >> 4));
    if (c !== 64) out += String.fromCharCode(((b & 15) << 4) | (c >> 2));
    if (d !== 64) out += String.fromCharCode(((c & 3) << 6) | d);
  }
  return out;
}

/**
 * Extract payload from a JWT token
 */
function decodeJwtPayload(token: string) {
  try {
    const parts = token.split(".");
    if (parts.length !== 3) return null;
    const json = decodeBase64(parts[1]);
    return JSON.parse(json);
  } catch (e) {
    return null;
  }
}

/**
 * Check if a token is expired or near expiry
 */
function isTokenExpired(token: string | null, bufferSeconds = 30) {
  if (!token) return true;
  const payload = decodeJwtPayload(token);
  if (!payload || !payload.exp) return true;
  const expiresAtMs = payload.exp * 1000;
  return Date.now() + bufferSeconds * 1000 >= expiresAtMs;
}

/**
 * Middleware to inject the authentication token into requests.
 * Now proactively refreshes token if it's about to expire.
 */
export const authMiddleware: Middleware = async ({ config, options }, next) => {
  let { token } = useAuthStore.getState();

  const isAuthPath =
    config.path === "auth/login" ||
    config.path === "auth/register" ||
    config.path === "auth/refresh" ||
    config.path === "auth/google/mobile" ||
    config.path === "auth/login-or-register-owner" ||
    config.path === "auth/forget-pin" ||
    config.path === "auth/forget-pin/verify";

  if (token && !isAuthPath) {
    // Proactive Expiration Check
    if (isTokenExpired(token)) {
      console.log(
        `[AuthProactive] Token near expiry or invalid. Refreshing proactively...`,
      );
      const newToken = await refreshTokens({
        path: config.path,
        type: "PROACTIVE_REFRESH",
      });
      if (newToken) {
        token = newToken;
      }
    }

    options.headers = {
      ...options.headers,
      Authorization: `Bearer ${token}`,
      "Content-Type": "application/json",
    };
  }

  return await next(options);
};

// Global promise to handle concurrent refreshes
let refreshPromise: Promise<string | null> | null = null;

/**
 * Proactively refreshes the access token using the current refresh token.
 * Coordinates multiple concurrent calls to ensure only one network request is made.
 */
export async function refreshTokens(
  config?: any,
  force = false,
): Promise<string | null> {
  const { refreshToken, token, setTokens, logout, isAuthenticated } =
    useAuthStore.getState();

  if (!isAuthenticated || !refreshToken) {
    return null;
  }

  // If not forced, check if we actually need a refresh
  if (!force && !isTokenExpired(token)) {
    return token;
  }

  // Coordination: If a refresh is already in progress, wait for it
  if (refreshPromise) {
    return await refreshPromise;
  }

  // No refresh in progress, start one
  refreshPromise = (async () => {
    try {
      const refreshUrl = `https://api.yaltopiaticket.com/auth/refresh`;

      console.log("[AuthRefresh] Starting network refresh...");

      const response = await fetch(refreshUrl, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ refreshToken }),
      });

      if (response.status === 401) {
        console.error(
          `[AuthRefresh] 401 on refresh. Session definitively dead. Path: ${config?.path || "Heartbeat"}`,
        );
        toast.show({
          type: "error",
          title: "Session Expired",
          message:
            "You have been logged out because your session has expired. Please sign in again.",
          duration: 9000,
        });
        logout();
        return null;
      }

      if (!response.ok) {
        const errorData = await response.json().catch(() => ({}));
        throw new Error(
          errorData.message || `Refresh failed: ${response.status}`,
        );
      }

      const data = await response.json();
      const newAT = data.accessToken || data.access_token;
      const newRT = data.refreshToken || data.refresh_token;

      if (!newAT) throw new Error("No access token in response");

      setTokens(newAT, newRT);
      console.log("[AuthRefresh] Tokens successfully updated.");
      return newAT;
    } catch (err: any) {
      console.error("[AuthRefresh] Refresh failed:", err.message);
      return null;
    } finally {
      refreshPromise = null;
    }
  })();

  return await refreshPromise;
}

/**
 * Middleware to handle token refreshment on 401 Unauthorized errors.
 * Includes a queue mechanism to handle concurrent 401s.
 */
export const refreshMiddleware: Middleware = async (
  { config, options },
  next,
) => {
  try {
    return await next(options);
  } catch (error: any) {
    const status = error.status || error.statusCode;
    const { refreshToken } = useAuthStore.getState();

    const isAuthPath =
      config.path?.includes("auth/login") ||
      config.path?.includes("auth/refresh") ||
      config.path?.includes("auth/google/mobile") ||
      config.path?.includes("auth/login-or-register-owner") ||
    config.path?.includes("auth/forget-pin");

    // Force refresh on 401 even if we think it's fresh (since server says it's not)
    if (status === 401 && !isAuthPath) {
      if (refreshToken) {
        console.log(
          `[API Refresh] 401 detected for ${config.path}. Forcing refresh...`,
        );
        try {
          const accessToken = await refreshTokens(config, true);

          if (!accessToken) {
            throw new Error("Failed to obtain fresh access token");
          }

          // Retry the original request
          console.log(`[API Refresh] Retrying ${config.path} with new token.`);
          options.headers = {
            ...options.headers,
            Authorization: `Bearer ${accessToken}`,
          };
          return await next(options);
        } catch (refreshError: any) {
          throw refreshError;
        }
      }
    }

    throw error;
  }
};
