import crypto from 'crypto';
import { db } from '@/db/client';
import { decrypt } from '@/lib/crypto/encryption';
import { ShopifyClient } from '@/lib/shopify/client';
import { getShopifyAccessToken } from '@/lib/shopify/token-manager';
import { ShopifyProductService } from '@/lib/shopify/product-service';
import { MetaCatalogService } from '@/lib/meta/catalog-service';
import { mapShopifyProductToMetaItems, ItemMapperOptions } from '@/lib/meta/item-mapper';
import { calculateNextSyncDate } from './scheduler';
import { MetaBatchItemRequest, CatalogItemRecord } from '@/types/catalog';
import { logger } from '@/lib/logger';

function hashItemData(data: Record<string, any>): string {
  return crypto.createHash('sha256').update(JSON.stringify(data)).digest('hex');
}

export interface SyncCatalogResult {
  success: boolean;
  catalogId: string;
  fbCatalogId: string;
  totalVariants: number;
  upsertedCount: number;
  deletedCount: number;
  error?: string;
}

export async function syncCatalog(catalogId: string): Promise<SyncCatalogResult> {
  const catalog = await db.getCatalogById(catalogId);
  if (!catalog) {
    throw new Error(`Catalog #${catalogId} not found`);
  }

  const shop = await db.getShopById(catalog.shop_id);
  if (!shop) {
    throw new Error(`Shop #${catalog.shop_id} for Catalog #${catalogId} not found`);
  }

  const nowIso = new Date().toISOString();

  // Set status to syncing
  await db.updateCatalog(catalogId, {
    status: 'syncing',
    updated_at: nowIso,
  });

  const syncLogId = `synclog_${catalogId}_${Date.now()}`;
  await db.createSyncLog({
    id: syncLogId,
    catalog_id: catalogId,
    trigger: 'manual',
    status: 'running',
    created_count: 0,
    updated_count: 0,
    deleted_count: 0,
    failed_count: 0,
    started_at: nowIso,
  });

  try {
    // 1. Resolve Meta Access Token
    const metaConn = await db.getMetaConnectionByShopId(shop.id);
    let metaAccessToken = process.env.META_SYSTEM_ACCESS_TOKEN || process.env.META_ACCESS_TOKEN || '';
    if (!metaAccessToken && metaConn?.access_token_encrypted && metaConn.status === 'connected') {
      try {
        metaAccessToken = decrypt(metaConn.access_token_encrypted);
      } catch {
        metaAccessToken = '';
      }
    }

    // 2. Refresh live Shopify products so all names, prices, images, and variants are up-to-date
    let liveRefreshError: string | null = null;
    try {
      let shopifyToken = '';
      try {
        shopifyToken = await getShopifyAccessToken(shop.shopify_domain);
      } catch {
        if (shop.access_token_encrypted) {
          try {
            const dec = decrypt(shop.access_token_encrypted);
            if (dec && !dec.startsWith('mock_')) {
              shopifyToken = dec;
            }
          } catch {}
        }
      }
      const client = new ShopifyClient(shop.shopify_domain, shopifyToken);
      const liveProducts = await client.fetchAllProductsForCatalog(shop.id);
      if (liveProducts && liveProducts.length > 0) {
        await db.clearProductsByShopId(shop.id);
        for (const lp of liveProducts) {
          await db.upsertProduct(lp);
        }
        logger.info(`[CatalogSyncService] Refreshed ${liveProducts.length} live Shopify products before catalog sync.`);
      }
    } catch (liveFetchErr: any) {
      liveRefreshError = liveFetchErr.message || String(liveFetchErr);
      logger.warn(`[CatalogSyncService] Live Shopify product refresh note: ${liveRefreshError}`);
    }

    // 3. Fetch selections
    const selections = await db.getCatalogSelections(catalogId);
    const collectionIds = selections
      .filter(s => s.type === 'collection' && !s.is_exclude)
      .flatMap(s => [
        s.shopify_gid.replace(/^gid:\/\/shopify\/Collection\//, ''),
        s.value,
      ])
      .filter(Boolean);
    const tags = selections
      .filter(s => s.type === 'tag' && !s.is_exclude)
      .map(s => s.value);
    const productIds = selections
      .filter(s => s.type === 'product' && !s.is_exclude)
      .map(s => s.shopify_gid.replace(/^gid:\/\/shopify\/Product\//, ''));

    const excludedTags = selections
      .filter(s => s.type === 'tag' && s.is_exclude)
      .map(s => s.value);
    const excludedCollectionIds = selections
      .filter(s => s.type === 'collection' && s.is_exclude)
      .flatMap(s => [
        s.shopify_gid.replace(/^gid:\/\/shopify\/Collection\//, ''),
        s.value,
      ])
      .filter(Boolean);
    const excludedProductIds = selections
      .filter(s => s.type === 'product' && s.is_exclude)
      .map(s => s.shopify_gid.replace(/^gid:\/\/shopify\/Product\//, ''));

    // 4. Fetch assigned products from Shopify database
    const shopifyProducts = await ShopifyProductService.getProductsForCatalog({
      shopDomain: shop.shopify_domain,
      accessToken: shop.access_token_encrypted ? (() => {
        try { return decrypt(shop.access_token_encrypted); } catch { return ''; }
      })() : '',
      syncType: catalog.sync_scope || 'all',
      collectionIds,
      tags,
      productIds,
      excludeOutOfStock: Boolean(catalog.exclude_out_of_stock),
      excludeTags: excludedTags.length > 0 ? excludedTags : undefined,
      excludeCollectionIds: excludedCollectionIds.length > 0 ? excludedCollectionIds : undefined,
      excludeProductIds: excludedProductIds.length > 0 ? excludedProductIds : undefined,
    });

    if (shopifyProducts.length === 0) {
      const finishIso = new Date().toISOString();
      const errReason = liveRefreshError
        ? `Shopify Product Sync Error: ${liveRefreshError}`
        : 'No products found in your Shopify store to sync. Please ensure active products exist in your store.';

      await db.updateCatalog(catalogId, {
        status: 'failed',
        product_count: 0,
        variant_count: 0,
        last_error: errReason,
        last_synced_at: finishIso,
        updated_at: finishIso,
      });

      await db.updateSyncLog(syncLogId, {
        status: 'failed',
        failed_count: 1,
        error_details: errReason,
        finished_at: finishIso,
      });

      return {
        success: false,
        catalogId,
        fbCatalogId: catalog.fb_catalog_id,
        totalVariants: 0,
        upsertedCount: 0,
        deletedCount: 0,
        error: errReason,
      };
    }

    // 5. Map Shopify products to Meta Catalog batch items
    const mapperOptions: ItemMapperOptions = {
      variantExportMode: catalog.variant_mode || 'all_separate',
      googleCategory: catalog.google_category_name || undefined,
      imageOption: catalog.additional_images ? 'all' : 'first',
      productLinkDomain: catalog.link_domain || 'myshopify',
      customDomain: catalog.custom_domain || undefined,
      shopDomain: shop.shopify_domain,
      currency: shopifyProducts[0]?.currency || 'USD',
      priceMode: catalog.price_mode || 'compare_at_and_price',
    };

    const allBatchRequests: MetaBatchItemRequest[] = [];
    const itemsToUpsertInDb: CatalogItemRecord[] = [];

    for (const prod of shopifyProducts) {
      const mapped = mapShopifyProductToMetaItems(prod, mapperOptions);
      for (const item of mapped) {
        allBatchRequests.push(item);
        const data = item.data || {};
        const contentHash = hashItemData(data);

        itemsToUpsertInDb.push({
          id: `item_${catalogId}_${item.retailer_id}`,
          catalog_id: catalogId,
          shopify_product_id: String(prod.id),
          shopify_variant_id: item.retailer_id,
          retailer_id: item.retailer_id,
          payload_hash: contentHash,
          fb_status: 'synced',
          synced_at: nowIso,
        });
      }
    }

    // Enforce store plan SKU quota (e.g. Free Plan: upto 100 SKUs)
    const plan = shop.plan ? await db.getPlan(shop.plan) : await db.getPlan('basic');
    const skuLimit = shop.plan_custom_sku_limit ?? (plan?.sku_limit ?? 100);
    if (skuLimit !== null && allBatchRequests.length > skuLimit) {
      logger.info(
        `[CatalogSyncService] Store ${shop.shopify_domain} plan '${shop.plan || 'basic'}' allows up to ${skuLimit} SKUs. Truncating batch from ${allBatchRequests.length} to ${skuLimit}.`
      );
      allBatchRequests.splice(skuLimit);
      itemsToUpsertInDb.splice(skuLimit);
    }

    // 5. Query existing items from DB AND from live Meta Catalog to find items to delete
    const existingDbItems = await db.getCatalogItems(catalogId);
    const existingRetailerIds = new Set<string>(existingDbItems.map(i => String(i.retailer_id)));

    // Reconcile with live Meta Catalog
    if (metaAccessToken && catalog.fb_catalog_id) {
      try {
        const liveMetaRetailerIds = await MetaCatalogService.fetchCatalogProductRetailerIds(
          catalog.fb_catalog_id,
          metaAccessToken
        );
        for (const mId of liveMetaRetailerIds) {
          existingRetailerIds.add(mId);
        }
        logger.info(`[CatalogSyncService] Reconciling against ${liveMetaRetailerIds.length} live items in Meta.`);
      } catch (metaFetchErr: any) {
        logger.warn(`[CatalogSyncService] Live Meta reconciliation error: ${metaFetchErr.message || metaFetchErr}`);
      }
    }

    const currentRetailerIds = new Set(itemsToUpsertInDb.map(i => String(i.retailer_id)));

    const retailerIdsToDelete: string[] = [];
    for (const rId of existingRetailerIds) {
      if (!currentRetailerIds.has(rId)) {
        retailerIdsToDelete.push(rId);
      }
    }

    // 6. Send to Meta Graph API if access token and fb_catalog_id are present
    let metaSyncErrors: Array<{ retailer_id: string; message: string }> = [];
    if (metaAccessToken && catalog.fb_catalog_id) {
      const BATCH_SIZE = 500;
      for (let i = 0; i < allBatchRequests.length; i += BATCH_SIZE) {
        const chunk = allBatchRequests.slice(i, i + BATCH_SIZE);
        try {
          const res = await MetaCatalogService.sendItemsBatch(catalog.fb_catalog_id, chunk, metaAccessToken);
          if (res.errors && res.errors.length > 0) {
            metaSyncErrors.push(...res.errors);
          }
          if (!res.success && res.rawError) {
            const isNetworkTimeout =
              res.rawError.includes('fetch failed') ||
              res.rawError.includes('timeout') ||
              res.rawError.includes('UND_ERR') ||
              res.rawError.includes('Network exception');
            if (!isNetworkTimeout) {
              metaSyncErrors.push({ retailer_id: 'batch', message: res.rawError });
            } else {
              console.warn('[CatalogSyncService] Meta batch network note:', res.rawError);
            }
          }
        } catch (batchErr: any) {
          console.warn('[CatalogSyncService] Meta batch send exception:', batchErr.message);
        }
      }

      // Send delete requests if any
      if (retailerIdsToDelete.length > 0) {
        const deleteBatch: MetaBatchItemRequest[] = retailerIdsToDelete.map(id => ({
          method: 'DELETE',
          retailer_id: id,
          data: { id },
        }));
        for (let i = 0; i < deleteBatch.length; i += BATCH_SIZE) {
          const chunk = deleteBatch.slice(i, i + BATCH_SIZE);
          try {
            const delRes = await MetaCatalogService.sendItemsBatch(catalog.fb_catalog_id, chunk, metaAccessToken);
            if (delRes.errors && delRes.errors.length > 0) {
              metaSyncErrors.push(...delRes.errors);
            }
          } catch (delErr: any) {
            console.warn('[CatalogSyncService] Meta batch delete exception:', delErr.message);
          }
        }
      }
    } else if (!metaAccessToken) {
      metaSyncErrors.push({
        retailer_id: 'meta_auth',
        message: 'Meta (Facebook) access token is missing or expired. Please connect or reconnect your Facebook account in the top-right.',
      });
    } else if (!catalog.fb_catalog_id) {
      metaSyncErrors.push({
        retailer_id: 'meta_catalog',
        message: 'Meta Catalog ID is missing. Please configure a valid Facebook Catalog in catalog settings.',
      });
    }

    // 7. Update database cache
    if (itemsToUpsertInDb.length > 0) {
      await db.upsertCatalogItems(catalogId, itemsToUpsertInDb);
    }
    if (retailerIdsToDelete.length > 0) {
      await db.deleteCatalogItems(catalogId, retailerIdsToDelete);
    }

    // 8. Calculate next sync
    let nextSync: string | undefined = undefined;
    if (catalog.auto_sync) {
      nextSync = calculateNextSyncDate(
        catalog.schedule_type || 'daily',
        catalog.schedule_day || 'Every Monday',
        catalog.schedule_hour || 2,
        catalog.schedule_period || 'AM',
        catalog.schedule_tz || 'UTC'
      );
    }

    // 9. Update catalog record
    const isCompleted = itemsToUpsertInDb.length > 0;
    const hasCriticalErrors = metaSyncErrors.length > 0 && (!isCompleted || metaSyncErrors.length >= itemsToUpsertInDb.length);
    const finishIso = new Date().toISOString();

    await db.updateCatalog(catalogId, {
      status: hasCriticalErrors ? 'failed' : 'synced',
      product_count: shopifyProducts.length,
      variant_count: itemsToUpsertInDb.length,
      last_synced_at: finishIso,
      next_sync_at: nextSync || null,
      last_error: metaSyncErrors.length > 0 ? metaSyncErrors[0]?.message : null,
      updated_at: finishIso,
    });

    // 10. Update sync log
    await db.updateSyncLog(syncLogId, {
      status: hasCriticalErrors ? 'failed' : 'completed',
      created_count: itemsToUpsertInDb.length,
      updated_count: itemsToUpsertInDb.length - metaSyncErrors.length,
      deleted_count: retailerIdsToDelete.length,
      failed_count: metaSyncErrors.length,
      error_details: metaSyncErrors.length > 0 ? JSON.stringify(metaSyncErrors.slice(0, 10)) : null,
      finished_at: finishIso,
    });

    return {
      success: !hasCriticalErrors,
      catalogId,
      fbCatalogId: catalog.fb_catalog_id,
      totalVariants: itemsToUpsertInDb.length,
      upsertedCount: itemsToUpsertInDb.length,
      deletedCount: retailerIdsToDelete.length,
      error: metaSyncErrors.length > 0 ? metaSyncErrors[0]?.message : undefined,
    };
  } catch (err: any) {
    console.error(`[CatalogSyncService] Sync failed for Catalog #${catalogId}:`, err);

    const finishIso = new Date().toISOString();

    await db.updateCatalog(catalogId, {
      status: 'failed',
      last_error: err.message || 'Unknown sync error',
      updated_at: finishIso,
    });

    await db.updateSyncLog(syncLogId, {
      status: 'failed',
      error_details: err.message || 'Unknown sync error',
      finished_at: finishIso,
    });

    return {
      success: false,
      catalogId,
      fbCatalogId: catalog.fb_catalog_id,
      totalVariants: 0,
      upsertedCount: 0,
      deletedCount: 0,
      error: err.message,
    };
  }
}
