import fs from 'fs';
import { db } from '@/db/client';
import { encrypt, decrypt } from '@/lib/crypto/encryption';
import { logger } from '@/lib/logger';
import {
  refreshShopifyAccessToken,
  exchangeLegacyTokenForExpiring,
  exchangeClientCredentialsGrant,
} from './oauth';
import { ShopRecord } from '@/types';

interface CachedToken {
  accessToken: string;
  expiresAt: number; // epoch ms
}

// In-memory token cache to avoid DB hits on high-frequency API calls
const memoryTokenCache = new Map<string, CachedToken>();

// Mutex lock map to eliminate race conditions & duplicate token refresh calls per shop
const inFlightRefreshes = new Map<string, Promise<string>>();

/**
 * Resolves the Shopify App Private Key from environment variable or file path if configured.
 * Used for RFC 7523 Client Credentials Grant with App Key Pairs.
 */
function resolveShopifyPrivateKey(): string | null {
  if (process.env.SHOPIFY_APP_PRIVATE_KEY) {
    return process.env.SHOPIFY_APP_PRIVATE_KEY.replace(/\\n/g, '\n').trim();
  }
  if (process.env.SHOPIFY_PRIVATE_KEY_PATH) {
    try {
      if (fs.existsSync(process.env.SHOPIFY_PRIVATE_KEY_PATH)) {
        return fs.readFileSync(process.env.SHOPIFY_PRIVATE_KEY_PATH, 'utf8').trim();
      }
    } catch (err: any) {
      logger.warn(`[TokenManager] Could not read SHOPIFY_PRIVATE_KEY_PATH: ${err.message}`);
    }
  }
  return null;
}

/**
 * Clears in-memory token cache for a store domain.
 */
export function clearCachedShopifyToken(shopDomain: string): void {
  const rawClean = shopDomain.replace(/^https?:\/\//, '').replace(/\/.*$/, '').trim().toLowerCase();
  const cleanDomain = rawClean.includes('.') ? rawClean : `${rawClean}.myshopify.com`;
  memoryTokenCache.delete(cleanDomain);
}

/**
 * Exchanges Shopify Client ID & Secret Key or App Private Key for a live Admin Access Token
 * via Shopify OAuth Client Credentials Grant.
 */
export async function exchangeClientCredentials(
  shopDomain: string,
  clientId: string,
  clientSecret: string
): Promise<{ accessToken: string; expiresIn: number; scope?: string }> {
  const rawClean = shopDomain.replace(/^https?:\/\//, '').replace(/\/.*$/, '').trim().toLowerCase();
  const cleanDomain = rawClean.includes('.') ? rawClean : `${rawClean}.myshopify.com`;

  logger.info(`[TokenManager] Requesting client credentials token for shop: ${cleanDomain}`);

  const privateKey = resolveShopifyPrivateKey();
  const credential = privateKey || clientSecret;

  const result = await exchangeClientCredentialsGrant(cleanDomain, clientId, credential);
  return {
    accessToken: result.access_token,
    expiresIn: result.expires_in,
    scope: result.scope,
  };
}

/**
 * Retrieves a valid, live Shopify Admin API Access Token for a shop domain.
 * Supports Shopify 2026/2027 Expiring Offline Access Tokens, auto-refreshes expiring tokens,
 * rotates refresh tokens atomically, handles concurrent requests with mutex locks,
 * and recovers via Client Credentials or token exchange if needed.
 */
export async function getShopifyAccessToken(
  shopDomain: string,
  forceRefresh = false
): Promise<string> {
  const rawClean = shopDomain.replace(/^https?:\/\//, '').replace(/\/.*$/, '').trim().toLowerCase();
  const cleanDomain = rawClean.includes('.') ? rawClean : `${rawClean}.myshopify.com`;

  // 1. Check in-memory cache if not force refreshing
  if (!forceRefresh) {
    const cached = memoryTokenCache.get(cleanDomain);
    if (cached && cached.expiresAt > Date.now() + 60 * 1000) {
      return cached.accessToken;
    }
  }

  // 2. Check if a refresh or token exchange is already in-flight for this shop
  const existingRefresh = inFlightRefreshes.get(cleanDomain);
  if (existingRefresh) {
    logger.info(`[TokenManager] Awaiting in-flight token refresh for ${cleanDomain}`);
    return existingRefresh;
  }

  // Create concurrency lock promise for this shop
  const refreshPromise = (async (): Promise<string> => {
    // Fetch app configuration & shop record from DB
    const [shopRecord, appConfig] = await Promise.all([
      db.getShopByDomain(cleanDomain),
      db.getAppConfigurationByDomain(cleanDomain),
    ]);

    // Resolve Client ID & Secret
    let clientId = appConfig?.shopify_api_key || process.env.SHOPIFY_API_KEY || '';
    let clientSecret = '';

    if (appConfig?.shopify_api_secret_encrypted) {
      try {
        clientSecret = decrypt(appConfig.shopify_api_secret_encrypted);
      } catch {
        // ignore
      }
    }
    if (!clientSecret) {
      clientSecret = process.env.SHOPIFY_API_SECRET || '';
    }

    // Check for direct static access token
    let staticToken: string | null = null;
    const encryptedToken = shopRecord?.access_token_encrypted || appConfig?.shopify_access_token_encrypted;
    if (encryptedToken) {
      try {
        const dec = decrypt(encryptedToken);
        if (dec && !dec.startsWith('mock_') && !dec.startsWith('shpat_mock_') && !dec.startsWith('shpat_demo_')) {
          staticToken = dec;
        }
      } catch (e: any) {
        logger.warn(`[TokenManager] Failed to decrypt stored token for ${cleanDomain}: ${e.message}`);
      }
    }

    // Check for stored refresh token
    let refreshToken: string | null = null;
    if (shopRecord?.refresh_token_encrypted) {
      try {
        const decRefresh = decrypt(shopRecord.refresh_token_encrypted);
        if (decRefresh && !decRefresh.startsWith('mock_')) {
          refreshToken = decRefresh;
        }
      } catch (e: any) {
        logger.warn(`[TokenManager] Failed to decrypt refresh token for ${cleanDomain}: ${e.message}`);
      }
    }

    const expiresAtMs = shopRecord?.token_expires_at ? new Date(shopRecord.token_expires_at).getTime() : null;
    // Buffer of 10 minutes to prevent mid-operation token expiration during long catalog syncs
    const isExpiringSoon = expiresAtMs ? Date.now() >= (expiresAtMs - 10 * 60 * 1000) : false;

    // 3. REFRESH FLOW: If refresh token exists and token is expiring or refresh is forced
    if (refreshToken && (forceRefresh || isExpiringSoon)) {
      try {
        logger.info(`[TokenManager] Refreshing Shopify access token for ${cleanDomain}`);
        const refreshed = await refreshShopifyAccessToken(cleanDomain, refreshToken, clientId, clientSecret);
        const newExpiresIn = refreshed.expires_in || 86400;
        const now = new Date();
        const newExpiresAt = new Date(now.getTime() + newExpiresIn * 1000).toISOString();
        const newRefreshExpiresAt = refreshed.refresh_token_expires_in
          ? new Date(now.getTime() + refreshed.refresh_token_expires_in * 1000).toISOString()
          : new Date(now.getTime() + 90 * 86400 * 1000).toISOString();
        const tokenUpdatedAt = now.toISOString();

        if (shopRecord) {
          const updatedRecord: ShopRecord = {
            ...shopRecord,
            access_token_encrypted: encrypt(refreshed.access_token),
            // Rotate refresh token atomically (Shopify sends a new refresh_token on each refresh)
            refresh_token_encrypted: refreshed.refresh_token ? encrypt(refreshed.refresh_token) : shopRecord.refresh_token_encrypted,
            token_expires_at: newExpiresAt,
            refresh_token_expires_at: newRefreshExpiresAt,
            token_updated_at: tokenUpdatedAt,
            updated_at: tokenUpdatedAt,
          };
          await db.upsertShop(updatedRecord);
        }

        memoryTokenCache.set(cleanDomain, {
          accessToken: refreshed.access_token,
          expiresAt: Date.now() + (newExpiresIn - 60) * 1000,
        });

        logger.info(`[TokenManager] Successfully refreshed and rotated access token for ${cleanDomain}`);
        return refreshed.access_token;
      } catch (refreshErr: any) {
        logger.error(`[TokenManager] Failed to refresh token for ${cleanDomain}:`, refreshErr.message);
      }
    }

    // 4. If we have a valid unexpired access token, return it
    if (staticToken && !forceRefresh && expiresAtMs && !isExpiringSoon) {
      memoryTokenCache.set(cleanDomain, {
        accessToken: staticToken,
        expiresAt: expiresAtMs - 60 * 1000,
      });
      return staticToken;
    }

    // 5. LEGACY MIGRATION FLOW: If legacy non-expiring offline token exists, migrate via Token Exchange
    if (staticToken && (forceRefresh || !shopRecord?.token_expires_at)) {
      try {
        logger.info(`[TokenManager] Migrating legacy token to expiring offline token for ${cleanDomain}`);
        const exchangeResult = await exchangeLegacyTokenForExpiring(cleanDomain, staticToken, clientId, clientSecret);
        const newExpiresIn = exchangeResult.expires_in || 86400;
        const now = new Date();
        const newExpiresAt = new Date(now.getTime() + newExpiresIn * 1000).toISOString();
        const newRefreshExpiresAt = exchangeResult.refresh_token_expires_in
          ? new Date(now.getTime() + exchangeResult.refresh_token_expires_in * 1000).toISOString()
          : new Date(now.getTime() + 90 * 86400 * 1000).toISOString();
        const tokenUpdatedAt = now.toISOString();

        if (shopRecord) {
          const updatedRecord: ShopRecord = {
            ...shopRecord,
            access_token_encrypted: encrypt(exchangeResult.access_token),
            refresh_token_encrypted: exchangeResult.refresh_token ? encrypt(exchangeResult.refresh_token) : null,
            token_expires_at: newExpiresAt,
            refresh_token_expires_at: newRefreshExpiresAt,
            token_updated_at: tokenUpdatedAt,
            updated_at: tokenUpdatedAt,
          };
          await db.upsertShop(updatedRecord);
        }

        memoryTokenCache.set(cleanDomain, {
          accessToken: exchangeResult.access_token,
          expiresAt: Date.now() + (newExpiresIn - 60) * 1000,
        });

        logger.info(`[TokenManager] Successfully migrated legacy token to expiring token for ${cleanDomain}`);
        return exchangeResult.access_token;
      } catch (exchangeErr: any) {
        logger.warn(`[TokenManager] Legacy token migration note for ${cleanDomain}: ${exchangeErr.message}`);
        if (!forceRefresh && staticToken) {
          memoryTokenCache.set(cleanDomain, {
            accessToken: staticToken,
            expiresAt: Date.now() + 60 * 1000,
          });
          return staticToken;
        }
      }
    }

    // 6. CLIENT CREDENTIALS FLOW: Emergency recovery when refresh token is lost / expired
    const privateKey = resolveShopifyPrivateKey();
    const credSecret = privateKey || clientSecret;
    if (clientId && credSecret) {
      try {
        const exchangeResult = await exchangeClientCredentialsGrant(cleanDomain, clientId, credSecret);
        const newToken = exchangeResult.access_token;
        const expiresIn = exchangeResult.expires_in;
        const expiresAt = Date.now() + (expiresIn - 300) * 1000;

        memoryTokenCache.set(cleanDomain, {
          accessToken: newToken,
          expiresAt,
        });

        const now = new Date().toISOString();
        if (appConfig) {
          await db.upsertAppConfiguration({
            ...appConfig,
            shopify_access_token_encrypted: encrypt(newToken),
            updated_at: now,
          });
        }

        logger.info(`[TokenManager] Acquired client credentials token for ${cleanDomain}`);
        return newToken;
      } catch (err: any) {
        logger.error(`[TokenManager] Error exchanging client credentials for ${cleanDomain}:`, err.message);
      }
    }

    // 7. Environment variable fallback (development only)
    const envToken = process.env.SHOPIFY_ADMIN_ACCESS_TOKEN;
    if (envToken && !envToken.startsWith('mock_')) {
      return envToken;
    }

    throw new Error(
      `No valid Shopify credentials found for store ${cleanDomain}. Please re-authorize the app on Shopify (/api/auth/shopify?shop=${cleanDomain}).`
    );
  })();

  inFlightRefreshes.set(cleanDomain, refreshPromise);

  try {
    return await refreshPromise;
  } finally {
    inFlightRefreshes.delete(cleanDomain);
  }
}

/**
 * Returns the current token health and expiration status for a shop.
 */
export async function getShopifyTokenStatus(shopDomain: string): Promise<{
  shopifyDomain: string;
  hasAccessToken: boolean;
  hasRefreshToken: boolean;
  isExpiringToken: boolean;
  isExpired: boolean;
  isExpiringSoon: boolean;
  tokenExpiresAt: string | null;
  refreshTokenExpiresAt: string | null;
  tokenUpdatedAt: string | null;
}> {
  const rawClean = shopDomain.replace(/^https?:\/\//, '').replace(/\/.*$/, '').trim().toLowerCase();
  const cleanDomain = rawClean.includes('.') ? rawClean : `${rawClean}.myshopify.com`;

  const shop = await db.getShopByDomain(cleanDomain);
  if (!shop) {
    return {
      shopifyDomain: cleanDomain,
      hasAccessToken: false,
      hasRefreshToken: false,
      isExpiringToken: false,
      isExpired: false,
      isExpiringSoon: false,
      tokenExpiresAt: null,
      refreshTokenExpiresAt: null,
      tokenUpdatedAt: null,
    };
  }

  const hasAccessToken = Boolean(shop.access_token_encrypted);
  const hasRefreshToken = Boolean(shop.refresh_token_encrypted);
  const isExpiringToken = Boolean(shop.token_expires_at);

  const expiresMs = shop.token_expires_at ? new Date(shop.token_expires_at).getTime() : null;
  const isExpired = expiresMs ? Date.now() >= expiresMs : false;
  const isExpiringSoon = expiresMs ? Date.now() >= (expiresMs - 10 * 60 * 1000) : false;

  return {
    shopifyDomain: cleanDomain,
    hasAccessToken,
    hasRefreshToken,
    isExpiringToken,
    isExpired,
    isExpiringSoon,
    tokenExpiresAt: shop.token_expires_at || null,
    refreshTokenExpiresAt: shop.refresh_token_expires_at || null,
    tokenUpdatedAt: shop.token_updated_at || shop.updated_at || null,
  };
}
