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

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

const memoryTokenCache = new Map<string, CachedToken>();

/**
 * Exchanges Shopify Client ID (API Key) & Secret Key for a live Admin Access Token
 * via Shopify OAuth Client Credentials Grant:
 * POST https://{shop}/admin/oauth/access_token
 * Body: client_id={id}&client_secret={secret}&grant_type=client_credentials
 */
export async function exchangeClientCredentials(
  shopDomain: string,
  clientId: string,
  clientSecret: string
): Promise<{ accessToken: string; expiresIn: number; scope?: string }> {
  const url = `https://${shopDomain}/admin/oauth/access_token`;
  const body = new URLSearchParams();
  body.append('client_id', clientId.trim());
  body.append('client_secret', clientSecret.trim());
  body.append('grant_type', 'client_credentials');

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

  const response = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
      'Accept': 'application/json',
    },
    body: body.toString(),
  });

  if (!response.ok) {
    const errorText = await response.text();
    logger.error(`[TokenManager] Client credentials exchange failed for ${shopDomain}: ${response.status} ${errorText}`);
    throw new Error(
      `Shopify client credentials exchange failed (${response.status}): ${errorText || response.statusText}`
    );
  }

  const data = await response.json();
  if (!data.access_token) {
    throw new Error(`Shopify oauth response missing access_token: ${JSON.stringify(data)}`);
  }

  return {
    accessToken: data.access_token,
    expiresIn: data.expires_in || 86400,
    scope: data.scope,
  };
}

/**
 * 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);
}

/**
 * Retrieves a valid, live Shopify Admin API Access Token for a shop domain.
 * Supports Shopify 2026 Expiring Offline Access Tokens, auto-refreshes expiring tokens,
 * and automatically exchanges legacy non-expiring offline tokens on the fly.
 */
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. Fetch app configuration & shop record from DB (prioritizing shop record)
  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 (prioritizing the merchant's OAuth install 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) {
      logger.warn(`[TokenManager] Failed to decrypt stored token for ${cleanDomain}: ${e instanceof Error ? e.message : String(e)}`);
    }
  }

  // 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) {
      logger.warn(`[TokenManager] Failed to decrypt refresh token for ${cleanDomain}: ${e instanceof Error ? e.message : String(e)}`);
    }
  }

  const expiresAtMs = shopRecord?.token_expires_at ? new Date(shopRecord.token_expires_at).getTime() : null;
  const isExpiringSoon = expiresAtMs ? Date.now() >= (expiresAtMs - 5 * 60 * 1000) : false;

  // 3. If we have a refresh token and token is expiring or force refresh is requested:
  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 newExpiresAt = new Date(Date.now() + newExpiresIn * 1000).toISOString();

      if (shopRecord) {
        const updatedRecord: ShopRecord = {
          ...shopRecord,
          access_token_encrypted: encrypt(refreshed.access_token),
          refresh_token_encrypted: refreshed.refresh_token ? encrypt(refreshed.refresh_token) : shopRecord.refresh_token_encrypted,
          token_expires_at: newExpiresAt,
          updated_at: new Date().toISOString(),
        };
        await db.upsertShop(updatedRecord);
      }

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

      return refreshed.access_token;
    } catch (refreshErr) {
      logger.error(`[TokenManager] Failed to refresh token for ${cleanDomain}:`, refreshErr);
    }
  }

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

  // 5. If we have a legacy non-expiring token (no token_expires_at recorded, or forceRefresh requested):
  // Migrate legacy offline token to expiring offline token 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 newExpiresAt = new Date(Date.now() + newExpiresIn * 1000).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,
          updated_at: new Date().toISOString(),
        };
        await db.upsertShop(updatedRecord);
      }

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

      return exchangeResult.access_token;
    } catch (exchangeErr) {
      logger.warn(`[TokenManager] Legacy token migration failed for ${cleanDomain}: ${exchangeErr instanceof Error ? exchangeErr.message : String(exchangeErr)}`);
      // If exchange fails and not forceRefresh, fallback to staticToken
      if (!forceRefresh) {
        memoryTokenCache.set(cleanDomain, {
          accessToken: staticToken,
          expiresAt: Date.now() + 60 * 1000,
        });
        return staticToken;
      }
    }
  }

  // If we still have staticToken without forceRefresh, use it
  if (staticToken && !forceRefresh) {
    memoryTokenCache.set(cleanDomain, {
      accessToken: staticToken,
      expiresAt: Date.now() + 3600 * 1000,
    });
    return staticToken;
  }

  // 6. If we have valid client credentials, attempt token exchange for app-level operations
  // IMPORTANT: NEVER overwrite shopRecord.access_token_encrypted with client credentials!
  if (clientId && clientSecret) {
    try {
      const exchangeResult = await exchangeClientCredentials(cleanDomain, clientId, clientSecret);
      const newToken = exchangeResult.accessToken;
      const expiresIn = exchangeResult.expiresIn;
      const expiresAt = Date.now() + (expiresIn - 300) * 1000;

      // Cache in memory only for temporary app operations
      memoryTokenCache.set(cleanDomain, {
        accessToken: newToken,
        expiresAt,
      });

      // Update app_configurations only (NOT shops table)
      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) {
      logger.error(`[TokenManager] Error exchanging client credentials for ${cleanDomain}:`, err);
    }
  }

  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}).`
  );
}
