import { tokenize } from "@/lib/text";
import { SEARCH_LIMITS, SORT_OPTIONS } from "@/lib/search-config";
import type { ProductSort, ProductFilters, ParsedSearchInput, PriceIntent, QueryAnalysis } from "@/types/search";
import type { Product } from "@/types/product";

/**
 * Query understanding, input validation and search-result suggestions.
 *
 * The phrase table below is the "query understanding" step of the pipeline. It
 * exists because a short catalogue cannot teach a latent space every synonym a
 * shopper might use, and it makes the intent of a query ("working out", "under
 * 100") explicit before anything is embedded.
 */

interface PhraseRule {
  pattern: RegExp;
  expansion: string[];
}

const PHRASE_RULES: PhraseRule[] = [
  {
    pattern: /\b(work\s?out|working\s?out|exercise|exercising|gym\s?session|at\s+the\s+gym|training)\b/,
    expansion: ["gym", "workout", "exercise", "training", "fitness"],
  },
  {
    pattern: /\b(travell?ing|travel|travelling|flight|flights|commute|commuting|journey|holiday|vacation|abroad)\b/,
    expansion: ["travel", "commute", "flight", "portable", "battery"],
  },
  {
    pattern: /\b(run|running|jogger|jogging|marathon|5k)\b/,
    expansion: ["running", "jogging", "cushioning", "breathable", "shoe"],
  },
  {
    pattern: /\b(music|listen|listening|song|songs|album|concert|band)\b/,
    expansion: ["music", "listening", "audio", "gift"],
  },
  {
    pattern: /\b(gift|gifts|present|presents|presents for|birthday|christmas|anniversary)\b/,
    expansion: ["gift", "present"],
  },
  {
    pattern: /\b(cheap|cheapest|budget|affordable|inexpensive|value|save|saving|thrift)\b/,
    expansion: ["affordable", "budget", "value"],
  },
  {
    pattern: /\b(premium|luxury|high.?end|top.?end|flagship|best\s+quality|professional)\b/,
    expansion: ["premium", "professional", "high"],
  },
  {
    pattern: /\b(cook|cooking|kitchen|recipe|meals?|dinner|breakfast)\b/,
    expansion: ["kitchen", "cooking", "non-stick", "durable"],
  },
  {
    pattern: /\b(skin|skincare|complexion|moisturis|moisturiz|hydrat|acne)\b/,
    expansion: ["skin", "hydrating", "serum", "daily"],
  },
  {
    pattern: /\b(read|reading|book|novel|literature|story)\b/,
    expansion: ["reading", "book", "paperback"],
  },
  {
    pattern: /\b(warm|winter|coat|jacket|cold|rain|rainy|waterproof)\b/,
    expansion: ["warm", "waterproof", "jacket", "layer"],
  },
  {
    pattern: /\b(office|work\s?at\s+home|study|productivity|laptop|desk)\b/,
    expansion: ["office", "laptop", "work"],
  },
  {
    pattern: /\b(relax|calm|sleep|sleeping|comfortable|comfort|pill|memory)\b/,
    expansion: ["comfortable", "memory", "soft", "rest"],
  },
];

const BUDGET_PATTERN =
  /\b(cheap(er|est)?|budget|affordable|inexpensive|lowest?\s+price|best\s+value|value\s+for\s+money|save|saving|less\s+expensive|under\s+budget)\b/;
const PREMIUM_PATTERN =
  /\b(premium|luxury|high[\s-]?end|top[\s-]?end|flagship|professional[\s-]?grade|best\s+quality)\b/;
const PRICE_CEILING_PATTERN =
  /\b(?:under|below|less\s+than|no\s+more\s+than|max(?:imum)?|up\s+to|within)\s*\$?\s*(\d+(?:\.\d{1,2})?)/;
const BARE_PRICE_PATTERN = /\$\s*(\d+(?:\.\d{1,2})?)/;

function detectPriceIntent(normalized: string): PriceIntent {
  if (BUDGET_PATTERN.test(normalized)) return "budget";
  if (PREMIUM_PATTERN.test(normalized)) return "premium";
  return "none";
}

function detectPriceCeiling(normalized: string): number | null {
  const ceiling = PRICE_CEILING_PATTERN.exec(normalized);
  if (ceiling) return Number.parseFloat(ceiling[1]);
  const bare = BARE_PRICE_PATTERN.exec(normalized);
  if (bare) return Number.parseFloat(bare[1]);
  return null;
}

/** Expand a raw query into the text that will actually be embedded. */
export function analyzeQuery(rawQuery: string): QueryAnalysis {
  const normalized = rawQuery.toLowerCase().trim();
  const terms = new Set(tokenize(normalized));

  for (const rule of PHRASE_RULES) {
    if (!rule.pattern.test(normalized)) continue;
    for (const term of tokenize(rule.expansion.join(" "))) terms.add(term);
  }

  return {
    expandedQuery: [...terms].join(" "),
    terms: [...terms],
    priceIntent: detectPriceIntent(normalized),
    priceCeiling: detectPriceCeiling(normalized),
  };
}

export type ParseResult =
  | { ok: true; value: ParsedSearchInput }
  | { ok: false; errors: Record<string, string[]> };

function parseNumberParam(
  raw: string | null,
  field: string,
  errors: Record<string, string[]>,
  { min, max }: { min: number; max: number },
): number | undefined {
  if (raw === null || raw.trim() === "") return undefined;
  const value = Number(raw);
  if (!Number.isFinite(value)) {
    errors[field] = ["must be a number"];
    return undefined;
  }
  if (value < min || value > max) {
    errors[field] = [`must be between ${min} and ${max}`];
    return undefined;
  }
  return value;
}

function parseFilters(
  params: URLSearchParams,
  errors: Record<string, string[]>,
): ProductFilters {
  const filters: ProductFilters = {};
  const category = params.get("category")?.trim();
  const brand = params.get("brand")?.trim();
  if (category) filters.category = category;
  if (brand) filters.brand = brand;

  const minPrice = parseNumberParam(params.get("minPrice"), "minPrice", errors, {
    min: 0,
    max: 100_000,
  });
  const maxPrice = parseNumberParam(params.get("maxPrice"), "maxPrice", errors, {
    min: 0,
    max: 100_000,
  });
  if (minPrice !== undefined) filters.minPrice = minPrice;
  if (maxPrice !== undefined) filters.maxPrice = maxPrice;

  const minRating = parseNumberParam(params.get("minRating"), "minRating", errors, {
    min: 0,
    max: 5,
  });
  if (minRating !== undefined) filters.minRating = minRating;

  if (params.get("inStock") === "true") filters.inStockOnly = true;

  if (
    filters.minPrice !== undefined &&
    filters.maxPrice !== undefined &&
    filters.minPrice > filters.maxPrice
  ) {
    errors.maxPrice = ["must be greater than or equal to minPrice"];
  }

  return filters;
}

/** Validate a request from the query string. Returns field-keyed errors. */
export function parseSearchParams(params: URLSearchParams): ParseResult {
  const errors: Record<string, string[]> = {};

  const rawQuery = params.get("q") ?? "";
  if (rawQuery.length > SEARCH_LIMITS.queryMaxLength) {
    errors.q = [`must be at most ${SEARCH_LIMITS.queryMaxLength} characters`];
  }
  const query = rawQuery.trim();

  const filters = parseFilters(params, errors);

  const rawSort = params.get("sort");
  let sort: ProductSort = "relevance";
  if (rawSort !== null && rawSort !== "") {
    if ((SORT_OPTIONS as readonly string[]).includes(rawSort)) {
      sort = rawSort as ProductSort;
    } else {
      errors.sort = [`must be one of ${SORT_OPTIONS.join(", ")}`];
    }
  }

  const limit =
    parseNumberParam(params.get("limit"), "limit", errors, {
      min: SEARCH_LIMITS.minLimit,
      max: SEARCH_LIMITS.maxLimit,
    }) ?? SEARCH_LIMITS.defaultLimit;

  if (Object.keys(errors).length > 0) return { ok: false, errors };
  return { ok: true, value: { query, filters, sort, limit } };
}

function readString(source: Record<string, unknown>, key: string): string | null {
  const value = source[key];
  return typeof value === "string" ? value : null;
}

/** Validate a JSON request body. Unknown fields are ignored. */
export function parseSearchBody(body: unknown): ParseResult {
  if (typeof body !== "object" || body === null || Array.isArray(body)) {
    return { ok: false, errors: { body: ["must be a JSON object"] } };
  }

  const source = body as Record<string, unknown>;
  const errors: Record<string, string[]> = {};

  const query = readString(source, "query") ?? readString(source, "q") ?? "";
  if (query.length > SEARCH_LIMITS.queryMaxLength) {
    errors.query = [`must be at most ${SEARCH_LIMITS.queryMaxLength} characters`];
  }

  const filtersSource =
    typeof source.filters === "object" && source.filters !== null && !Array.isArray(source.filters)
      ? (source.filters as Record<string, unknown>)
      : {};

  const params = new URLSearchParams();
  for (const key of ["category", "brand"] as const) {
    const value = readString(filtersSource, key);
    if (value) params.set(key, value);
  }
  for (const key of ["minPrice", "maxPrice", "minRating"] as const) {
    const value = filtersSource[key];
    if (typeof value === "number" && Number.isFinite(value)) params.set(key, String(value));
    else if (typeof value === "string" && value !== "") params.set(key, value);
    else if (value !== undefined && value !== null) errors[key] = ["must be a number"];
  }
  if (filtersSource.inStockOnly === true) params.set("inStock", "true");

  const filters = parseFilters(params, errors);

  const rawSort = readString(source, "sort");
  let sort: ProductSort = "relevance";
  if (rawSort) {
    if ((SORT_OPTIONS as readonly string[]).includes(rawSort)) {
      sort = rawSort as ProductSort;
    } else {
      errors.sort = [`must be one of ${SORT_OPTIONS.join(", ")}`];
    }
  }

  const limitValue = source.limit;
  const limit =
    typeof limitValue === "number" && Number.isFinite(limitValue)
      ? limitValue
      : SEARCH_LIMITS.defaultLimit;
  if (limit < SEARCH_LIMITS.minLimit || limit > SEARCH_LIMITS.maxLimit) {
    errors.limit = [`must be between ${SEARCH_LIMITS.minLimit} and ${SEARCH_LIMITS.maxLimit}`];
  }

  if (Object.keys(errors).length > 0) return { ok: false, errors };
  return { ok: true, value: { query: query.trim(), filters, sort, limit } };
}

/** Serialise search state for the URL, omitting defaults to keep it readable. */
export function toSearchParams(input: ParsedSearchInput): URLSearchParams {
  const params = new URLSearchParams();
  if (input.query) params.set("q", input.query);
  if (input.filters.category) params.set("category", input.filters.category);
  if (input.filters.brand) params.set("brand", input.filters.brand);
  if (input.filters.minPrice !== undefined) params.set("minPrice", String(input.filters.minPrice));
  if (input.filters.maxPrice !== undefined) params.set("maxPrice", String(input.filters.maxPrice));
  if (input.filters.minRating !== undefined) params.set("minRating", String(input.filters.minRating));
  if (input.filters.inStockOnly) params.set("inStock", "true");
  if (input.sort !== "relevance") params.set("sort", input.sort);
  return params;
}

export function matchesFilters(product: Product, filters: ProductFilters): boolean {
  if (filters.category && product.category !== filters.category) return false;
  if (filters.brand && product.brand !== filters.brand) return false;
  if (filters.minPrice !== undefined && product.price < filters.minPrice) return false;
  if (filters.maxPrice !== undefined && product.price > filters.maxPrice) return false;
  if (filters.minRating !== undefined && product.rating < filters.minRating) return false;
  if (filters.inStockOnly && product.stock <= 0) return false;
  return true;
}

export function hasActiveFilters(filters: ProductFilters): boolean {
  return (
    Boolean(filters.category) ||
    Boolean(filters.brand) ||
    filters.minPrice !== undefined ||
    filters.maxPrice !== undefined ||
    filters.minRating !== undefined ||
    Boolean(filters.inStockOnly)
  );
}

/**
 * Suggest tags and subcategories that overlap the query, for the search box
 * dropdown. Deliberately lexical: a suggestion is only useful if the user can
 * see the literal string.
 */
export function buildSuggestions(
  query: string,
  products: Product[],
  limit = 6,
): string[] {
  const terms = tokenize(query);
  if (terms.length === 0) return [];

  const candidates = new Map<string, number>();
  for (const product of products) {
    const entries: [string, number][] = [
      [product.subcategory, 2],
      ...product.tags.map((tag) => [tag, 1] as [string, number]),
    ];
    for (const [value, weight] of entries) {
      const normalized = value.toLowerCase();
      const tokens = tokenize(normalized);
      if (tokens.length === 0) continue;

      let overlap = 0;
      for (const term of terms) {
        if (tokens.includes(term)) overlap += weight;
      }
      if (overlap > 0) {
        candidates.set(value, (candidates.get(value) ?? 0) + overlap);
      }
    }
  }

  return [...candidates.entries()]
    .sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]))
    .slice(0, limit)
    .map(([value]) => value);
}
