import { ShopifyAdminClient } from "../lib/shopify/admin";
import { getTenantByDomain } from "../lib/security/tenant-context";
import { detectStoreCurrency } from "../lib/currency";
import prisma from "../db.server";

export interface ParsedSearchQuery {
  keywords: string[];
  rawKeywordsString: string;
  minPrice?: number;
  maxPrice?: number;
  isBrowseAll: boolean;
  isLatest?: boolean;
  isCheapest?: boolean;
  isMostExpensive?: boolean;
  isOnSale?: boolean;
}

export interface SearchProductResult {
  success: boolean;
  status: "SUCCESS" | "NO_MATCH" | "TECHNICAL_ERROR";
  products: Array<{
    id: string;
    shopifyProductId: string;
    title: string;
    handle: string;
    description: string;
    image: string | null;
    minPrice: number;
    maxPrice: number;
    currency: string;
    variants: any[];
  }>;
  message?: string;
}

/**
 * Robust Natural-Language Search Query Parser.
 * Accurately extracts price constraints, browsing intents, latest product intents, typos, and keyword tokens.
 */
export function parseProductSearchQuery(rawQuery: string): ParsedSearchQuery {
  if (!rawQuery) {
    return { keywords: [], rawKeywordsString: "", isBrowseAll: true, isLatest: false };
  }

  // 1. Phonetic, Common Typos & Synonyms Normalizations
  let normalized = rawQuery
    .toLowerCase()
    .trim()
    .replace(/[’']s\b/g, "")
    .replace(/[’'"]/g, " ")
    .replace(/\b(tshirt|t-shirt|t_shirt|tshirts|t-shirts)\b/g, "tshirt shirt tee")
    .replace(/\b(prodcts?|prods?|prodcuts?|prdoucts?)\b/g, "products")
    .replace(/\b(shooes|shose|shoos|sheos)\b/g, "shoes")
    .replace(/\b(sneaker|sneakers)\b/g, "shoes sneaker")
    .replace(/\b(recomend|recommed|reccomend|recomended|recomends?)\b/g, "recommend")
    .replace(/\b(wher|whre)\b/g, "where")
    .replace(/\b(stok|stoc)\b/g, "stock")
    .replace(/\b(cheep|cheeper)\b/g, "cheap")
    .replace(/\b(somthing|smth|someting)\b/g, "something")
    .replace(/\b(wat|wot)\b/g, "what")
    .replace(/\bu\b/g, "you")
    .replace(/\bur\b/g, "your");

  // 2. Detect Intent Modifiers (latest, cheapest, most expensive, on sale)
  const isLatest = /\b(latest|newest|new arrivals?|recently added|recent|new items?|new products?|fresh)\b/i.test(normalized);
  const isCheapest = /\b(cheapest|cheaper|cheap|lowest price|lowest cost|affordable)\b/i.test(normalized);
  const isMostExpensive = /\b(most expensive|highest price|priciest|premium|luxury)\b/i.test(normalized);
  const isOnSale = /\b(on sale|sale|discounted|deals?|bargains?)\b/i.test(normalized);

  // 3. Extract Price Range & Budget: "between $50 and $100", "$200 budget", "for $50"
  let minPrice: number | undefined = undefined;
  let maxPrice: number | undefined = undefined;

  const rangeRegex = /(?:between|from)\s*(?:[\$₹£€]|rs\.?|rupees|inr|usd|dollars?|bucks|eur|gbp)?\s*(\d+(?:\.\d+)?)\s*(?:and|to|-)\s*(?:[\$₹£€]|rs\.?|rupees|inr|usd|dollars?|bucks|eur|gbp)?\s*(\d+(?:\.\d+)?)/i;
  const rangeMatch = normalized.match(rangeRegex);

  if (rangeMatch) {
    const p1 = parseFloat(rangeMatch[1]);
    const p2 = parseFloat(rangeMatch[2]);
    if (!isNaN(p1) && !isNaN(p2)) {
      minPrice = Math.min(p1, p2);
      maxPrice = Math.max(p1, p2);
    }
  } else {
    // Budget expressions: "budget of $200", "$200 budget", "for $50", "get for $100"
    const budgetRegex = /(?:budget (?:of|is)?|have a|within (?:my)? budget (?:of)?|for|get for|can i get for|around|approx)\s*(?:[\$₹£€]|rs\.?|rupees|inr|usd|dollars?|bucks|eur|gbp)?\s*(\d+(?:\.\d+)?)/i;
    const budgetMatch = normalized.match(budgetRegex);

    if (budgetMatch) {
      const parsedBudget = parseFloat(budgetMatch[1]);
      if (!isNaN(parsedBudget) && parsedBudget > 0) {
        maxPrice = parsedBudget;
      }
    }

    // Under / Max price: "under $50", "below 100rs", "under Rs 100", "up to $150"
    const maxPriceRegex = /(?:under|below|less than|within|max|up to)\s*(?:[\$₹£€]|rs\.?|rupees|inr|usd|dollars?|bucks|eur|gbp)?\s*(\d+(?:\.\d+)?)\s*(?:rs\.?|rupees|inr|usd|dollars?|bucks|eur|gbp)?/i;
    const maxMatch = normalized.match(maxPriceRegex);
    if (maxMatch) {
      const rawNum = maxMatch[1];
      if (rawNum) {
        const parsed = parseFloat(rawNum);
        if (!isNaN(parsed) && parsed > 0) {
          maxPrice = parsed;
        }
      }
    }

    // Over / Min price: "over $50", "above 100rs", "over Rs 100"
    const minPriceRegex = /(?:over|above|more than|greater than|min|at least)\s*(?:[\$₹£€]|rs\.?|rupees|inr|usd|dollars?|bucks|eur|gbp)?\s*(\d+(?:\.\d+)?)\s*(?:rs\.?|rupees|inr|usd|dollars?|bucks|eur|gbp)?/i;
    const minMatch = normalized.match(minPriceRegex);
    if (minMatch) {
      const rawNum = minMatch[1];
      if (rawNum) {
        const parsed = parseFloat(rawNum);
        if (!isNaN(parsed) && parsed > 0) {
          minPrice = parsed;
        }
      }
    }
  }

  // 4. Remove price clauses, intent words, and conversational fillers to extract core keywords
  let cleaned = normalized
    .replace(/(?:between|from)\s*(?:[\$₹£€]|rs\.?|rupees|inr|usd|dollars?|bucks|eur|gbp)?\s*\d+(?:\.\d+)?\s*(?:and|to|-)\s*(?:[\$₹£€]|rs\.?|rupees|inr|usd|dollars?|bucks|eur|gbp)?\s*\d+(?:\.\d+)?\s*(?:rs\.?|rupees|inr|usd|dollars?|bucks|eur|gbp)?/gi, " ")
    .replace(/(?:under|below|less than|within|max|up to|over|above|more than|at least|budget (?:of|is)?|have a|within (?:my)? budget (?:of)?|for|get for|can i get for|around|approx)\s*(?:[\$₹£€]|rs\.?|rupees|inr|usd|dollars?|bucks|eur|gbp)?\s*\d+(?:\.\d+)?\s*(?:rs\.?|rupees|inr|usd|dollars?|bucks|eur|gbp)?/gi, " ")
    .replace(/\b(i|want|to|and|or|for|of|in|your|store|find|show|me|some|all|everything|anything|any|got|there|are|have|has|with|like|much|many|such|stuff|thing|things|products?|items?|catalog|collection|collections?|looking|do|you|is|it|its|this|that|these|those|on|available|what|can|get|give|please|the|a|an|browse|view|display|recommendations?|recommend|suggestions?|suggest|good|best|popular|latest|newest|new|recent|recently|added|arrivals?|fresh|trending|cheapest|cheaper|cheap|affordable|expensive|luxury|premium|sale|discounted|deals?|sell|sold|something|interesting|options?|option|choose|need|tell|which|one|about|help|shop|pair|budget|right|now|top|pick|picks|better|gift|gifts|birthday|friend|everyday|use|difference|compare)\b/gi, " ")
    .replace(/[^\w\s]/g, " ")
    .replace(/\s+/g, " ")
    .trim();

  const keywords = cleaned ? cleaned.split(/\s+/).filter((w) => w.length >= 2) : [];

  // 5. Determine if this is a general browse request or specific search
  const isBrowseAll =
    keywords.length === 0 ||
    /^(all|everything|anything|any|catalog|some products|products|items|recommendations?|latest|new|recent|new arrivals|cheapest|sale|options?|something|expensive|popular|top pick|top picks|best|gift|gifts|birthday)$/i.test(normalized.trim());

  return {
    keywords,
    rawKeywordsString: cleaned,
    minPrice,
    maxPrice,
    isBrowseAll,
    isLatest,
    isCheapest,
    isMostExpensive,
    isOnSale,
  };
}

/**
 * Searches the store catalog with strict tenant isolation, server-side price filtering, and relevance ranking.
 * Never fabricates products or falls back to arbitrary unrelated items on specific searches.
 */
export async function searchProductsService(
  shopDomain: string,
  query: string,
  explicitMaxPrice?: number,
  explicitMinPrice?: number
): Promise<SearchProductResult> {
  try {
    const tenant = await getTenantByDomain(shopDomain);
    const parsed = parseProductSearchQuery(query);
    const effectiveMaxPrice = explicitMaxPrice ?? parsed.maxPrice;
    const effectiveMinPrice = explicitMinPrice ?? parsed.minPrice;
    const { keywords, rawKeywordsString, isBrowseAll, isLatest, isCheapest, isMostExpensive, isOnSale } = parsed;

    // Helper to score and rank product relevance generically across ANY Shopify store catalog
    const rankProducts = (items: any[]) => {
      if (isBrowseAll || keywords.length === 0) {
        return items;
      }

      // Generate stem forms of keywords
      const getStems = (word: string): string[] => {
        const set = new Set<string>([word]);
        if (word.endsWith("es") && word.length > 4) set.add(word.slice(0, -2));
        if (word.endsWith("s") && word.length > 3) set.add(word.slice(0, -1));
        if (word.endsWith("ing") && word.length > 5) set.add(word.slice(0, -3));
        if (word.endsWith("ed") && word.length > 4) set.add(word.slice(0, -2));
        return Array.from(set);
      };

      const queryKeywordStems = keywords.map((k) => ({
        original: k,
        stems: getStems(k),
        compact: k.replace(/[^a-z0-9]/g, ""),
      }));

      return items
        .map((p) => {
          const title = (p.title || "").toLowerCase();
          const cleanTitle = title.replace(/[-_]/g, " ");
          const compactTitle = title.replace(/[^a-z0-9]/g, "");
          const desc = (p.description || "").toLowerCase();
          const cleanDesc = desc.replace(/[-_]/g, " ");
          const handle = (p.handle || "").toLowerCase();
          const cleanHandle = handle.replace(/[-_]/g, " ");

          // Extract metadata tags, productType, and vendor
          let metaTags = "";
          let metaType = "";
          let metaVendor = "";
          if (p.metadata) {
            try {
              const meta = typeof p.metadata === "string" ? JSON.parse(p.metadata) : p.metadata;
              metaTags = Array.isArray(meta.tags) ? meta.tags.join(" ").toLowerCase() : String(meta.tags || "").toLowerCase();
              metaType = String(meta.productType || "").toLowerCase();
              metaVendor = String(meta.vendor || "").toLowerCase();
            } catch {}
          }

          // Extract variant option values (colors, sizes, styles)
          let variantText = "";
          if (p.variants) {
            try {
              const vars = typeof p.variants === "string" ? JSON.parse(p.variants) : p.variants;
              if (Array.isArray(vars)) {
                variantText = vars.map((v: any) => v.title || "").join(" ").toLowerCase();
              }
            } catch {}
          }

          let score = 0;
          if (rawKeywordsString && (title === rawKeywordsString || cleanTitle === rawKeywordsString)) {
            score += 150; // Exact full title match
          } else if (rawKeywordsString && (title.includes(rawKeywordsString) || cleanTitle.includes(rawKeywordsString))) {
            score += 100; // Exact substring match
          }

          let matchedKeywordCount = 0;

          for (const kwObj of queryKeywordStems) {
            let matched = false;

            // 1. Title match (Highest weight)
            const inTitle = kwObj.stems.some(
              (st) => title.includes(st) || cleanTitle.includes(st) || compactTitle.includes(kwObj.compact)
            );
            if (inTitle) {
              score += 50;
              matched = true;
            }

            // 2. Product Type / Tags / Vendor match
            const inMeta = kwObj.stems.some(
              (st) => metaType.includes(st) || metaTags.includes(st) || metaVendor.includes(st)
            );
            if (inMeta) {
              score += 35;
              matched = true;
            }

            // 3. Variant Option (Color, Size, Style) match
            const inVariant = kwObj.stems.some((st) => variantText.includes(st));
            if (inVariant) {
              score += 25;
              matched = true;
            }

            // 4. Handle match
            const inHandle = kwObj.stems.some(
              (st) => handle.includes(st) || cleanHandle.includes(st)
            );
            if (inHandle) {
              score += 20;
              matched = true;
            }

            // 5. Description match
            const inDesc = kwObj.stems.some(
              (st) => desc.includes(st) || cleanDesc.includes(st)
            );
            if (inDesc) {
              score += 10;
              matched = true;
            }

            if (matched) {
              matchedKeywordCount++;
            }
          }

          // Full Conjunction Bonus: All search tokens matched the product (e.g. "bamboo" AND "toothbrush")
          if (matchedKeywordCount === queryKeywordStems.length && queryKeywordStems.length > 1) {
            score += 60;
          }

          return { ...p, _score: score, _matched: matchedKeywordCount > 0 };
        })
        .filter((p) => p._matched || isBrowseAll)
        .sort((a, b) => b._score - a._score);
    };

    // 1. Search Local Cached Products for this Tenant (Ordered appropriately)
    let cachedProducts = await prisma.productCache.findMany({
      where: {
        shopId: tenant.shopId,
        ...(effectiveMaxPrice !== undefined || effectiveMinPrice !== undefined
          ? {
              minPrice: {
                ...(effectiveMinPrice !== undefined ? { gte: effectiveMinPrice } : {}),
                ...(effectiveMaxPrice !== undefined ? { lte: effectiveMaxPrice } : {}),
              },
            }
          : {}),
      },
      orderBy: isMostExpensive
        ? { maxPrice: "desc" }
        : isCheapest
        ? { minPrice: "asc" }
        : { updatedAt: "desc" },
      take: 50,
    });

    if (isOnSale) {
      const saleItems = cachedProducts.filter((p) => {
        const hasDiscount = p.variants && Array.isArray(p.variants)
          ? p.variants.some((v: any) => v.compareAtPrice && v.compareAtPrice > v.price)
          : false;
        return hasDiscount;
      });
      if (saleItems.length > 0) {
        cachedProducts = saleItems;
      }
    }

    let rankedCached = rankProducts(cachedProducts);

    if (rankedCached.length > 0) {
      const topItems = rankedCached.slice(0, 6).map((p) => ({
        id: p.id,
        shopifyProductId: p.shopifyProductId,
        title: p.title,
        handle: p.handle,
        description: p.description || "",
        image: p.image,
        minPrice: parseFloat(String(p.minPrice || 0)),
        maxPrice: parseFloat(String(p.maxPrice || p.minPrice || 0)),
        currency: p.currency || "USD",
        variants: typeof p.variants === "string" ? JSON.parse(p.variants) : (p.variants || []),
      }));

      return {
        success: true,
        status: "SUCCESS",
        products: topItems,
      };
    }

    // 2. If Local Cache is empty, attempt Live Shopify Ingestion
    if (cachedProducts.length === 0) {
      await ingestStorefrontCatalog(shopDomain, tenant.shopId);

      // Re-query local cache after ingestion
      const freshProducts = await prisma.productCache.findMany({
        where: {
          shopId: tenant.shopId,
          ...(effectiveMaxPrice ? { minPrice: { lte: effectiveMaxPrice } } : {}),
        },
        take: 50,
      });

      const freshRanked = rankProducts(freshProducts);

      if (freshRanked.length > 0) {
        const topItems = freshRanked.slice(0, 6).map((p) => ({
          id: p.id,
          shopifyProductId: p.shopifyProductId,
          title: p.title,
          handle: p.handle,
          description: p.description || "",
          image: p.image,
          minPrice: parseFloat(String(p.minPrice || 0)),
          maxPrice: parseFloat(String(p.maxPrice || p.minPrice || 0)),
          currency: p.currency || "USD",
          variants: typeof p.variants === "string" ? JSON.parse(p.variants) : (p.variants || []),
        }));

        return {
          success: true,
          status: "SUCCESS",
          products: topItems,
        };
      }
    }

    // 3. Specific Search yielded NO MATCH in Catalog
    return {
      success: true,
      status: "NO_MATCH",
      products: [],
      message: `I couldn't find any products matching "${rawKeywordsString || query}" in the store. Would you like to try another category, style, or price range?`,
    };
  } catch (error: any) {
    console.error("[searchProductsService] Failed to query store products:", error);
    return {
      success: false,
      status: "TECHNICAL_ERROR",
      products: [],
      message: "Unable to connect to the store catalog right now. Please try again in a moment.",
    };
  }
}

/**
 * Live Ingestion Helper: Ingests store products from Shopify Admin GraphQL API or Storefront public JSON.
 * Verified live data only — no fabricated inventory.
 */
async function ingestStorefrontCatalog(shopDomain: string, shopId: string): Promise<void> {
  const cleanDomain = shopDomain.replace(/^https?:\/\//, "").replace(/\/.*$/, "");
  const storeCurrency = await detectStoreCurrency(cleanDomain);

  // Method A: Admin GraphQL API
  try {
    const adminClient = new ShopifyAdminClient({ shopifyDomain: cleanDomain });
    const result = await adminClient.searchProducts("", 50);
    const edges = result?.products?.edges || [];

    if (edges.length > 0) {
      for (const edge of edges) {
        const node = edge.node;
        const minPrice = parseFloat(node.priceRangeV2?.minVariantPrice?.amount || "0");
        const maxPriceVal = parseFloat(node.priceRangeV2?.maxVariantPrice?.amount || "0");
        const currency = node.priceRangeV2?.minVariantPrice?.currencyCode || storeCurrency;

        const variants = (node.variants?.edges || []).map((vEdge: any) => ({
          shopifyVariantId: String(vEdge.node.id),
          title: vEdge.node.title,
          price: parseFloat(vEdge.node.price || "0"),
          compareAtPrice: vEdge.node.compareAtPrice ? parseFloat(vEdge.node.compareAtPrice) : null,
          available: vEdge.node.availableForSale !== false,
          inventoryQuantity: vEdge.node.inventoryQuantity ?? null,
        }));

        const metadata = {
          productType: node.productType || "",
          tags: node.tags || [],
          vendor: node.vendor || "",
        };

        await prisma.productCache.upsert({
          where: {
            shopId_shopifyProductId: {
              shopId,
              shopifyProductId: String(node.id),
            },
          },
          update: {
            title: node.title,
            handle: node.handle,
            description: node.description || "",
            image: node.featuredImage?.url || null,
            minPrice,
            maxPrice: maxPriceVal,
            currency,
            variants,
            metadata,
          },
          create: {
            shopId,
            shopifyProductId: String(node.id),
            title: node.title,
            handle: node.handle,
            description: node.description || "",
            image: node.featuredImage?.url || null,
            minPrice,
            maxPrice: maxPriceVal,
            currency,
            variants,
            metadata,
          },
        });
      }
      return;
    }
  } catch (adminErr) {}

  // Method B: Storefront public JSON
  try {
    const res = await fetch(`https://${cleanDomain}/products.json?limit=50`);
    if (res.ok) {
      const data = await res.json();
      const rawProducts = data.products || [];

      for (const p of rawProducts) {
        const variants = (p.variants || []).map((v: any) => ({
          shopifyVariantId: String(v.id),
          title: v.title || "Default Title",
          price: parseFloat(v.price || "0"),
          compareAtPrice: v.compare_at_price ? parseFloat(v.compare_at_price) : null,
          available: v.available !== false,
          inventoryQuantity: null,
        }));

        const prices = variants.map((v: any) => v.price).filter((pr: number) => !isNaN(pr));
        const minPrice = prices.length > 0 ? Math.min(...prices) : 0;
        const maxPriceVal = prices.length > 0 ? Math.max(...prices) : 0;

        const metadata = {
          productType: p.product_type || "",
          tags: p.tags || [],
          vendor: p.vendor || "",
        };

        await prisma.productCache.upsert({
          where: {
            shopId_shopifyProductId: {
              shopId,
              shopifyProductId: String(p.id),
            },
          },
          update: {
            title: p.title,
            handle: p.handle,
            description: p.body_html?.replace(/<[^>]*>?/gm, "") || "",
            image: p.images?.[0]?.src || null,
            minPrice,
            maxPrice: maxPriceVal,
            currency: storeCurrency,
            variants,
            metadata,
          },
          create: {
            shopId,
            shopifyProductId: String(p.id),
            title: p.title,
            handle: p.handle,
            description: p.body_html?.replace(/<[^>]*>?/gm, "") || "",
            image: p.images?.[0]?.src || null,
            minPrice,
            maxPrice: maxPriceVal,
            currency: storeCurrency,
            variants,
            metadata,
          },
        });
      }
    }
  } catch (storefrontErr) {}
}
