import { query, mutation, internalQuery, internalMutation } from "./_generated/server";
import { paginationOptsValidator } from "convex/server";
import { v } from "convex/values";
import type { Id, Doc } from "./_generated/dataModel";
import * as Users from "./model/users";
import { getProductWithRelations, enrichProducts } from "./model/products";

export const list = query({
  args: {
    paginationOpts: paginationOptsValidator,
    status: v.optional(v.string()),
    categoryId: v.optional(v.id("categories")),
  },
  handler: async (ctx, args) => {
    let q;

    if (args.status && args.categoryId) {
      q = ctx.db
        .query("products")
        .withIndex("by_status_and_category", (idx) =>
          idx.eq("status", args.status as any).eq("categoryId", args.categoryId!),
        );
    } else if (args.status) {
      q = ctx.db
        .query("products")
        .withIndex("by_status", (idx) => idx.eq("status", args.status as any));
    } else if (args.categoryId) {
      q = ctx.db
        .query("products")
        .withIndex("by_category", (idx) =>
          idx.eq("categoryId", args.categoryId!),
        );
    } else {
      q = ctx.db.query("products");
    }

    const result = await q.paginate(args.paginationOpts);

    const enrichedPage = await enrichProducts(ctx, result.page as any);

    return { ...result, page: enrichedPage };
  },
});

const LIST_ALL_LIMIT = 500;

export const listAll = query({
  args: {
    status: v.optional(v.string()),
    categoryId: v.optional(v.id("categories")),
    limit: v.optional(v.number()),
  },
  handler: async (ctx, args) => {
    const limit = Math.min(args.limit ?? LIST_ALL_LIMIT, 1000);
    let q;

    if (args.status && args.categoryId) {
      q = ctx.db
        .query("products")
        .withIndex("by_status_and_category", (idx) =>
          idx.eq("status", args.status as any).eq("categoryId", args.categoryId!),
        );
    } else if (args.status) {
      q = ctx.db
        .query("products")
        .withIndex("by_status", (idx) => idx.eq("status", args.status as any));
    } else if (args.categoryId) {
      q = ctx.db
        .query("products")
        .withIndex("by_category", (idx) =>
          idx.eq("categoryId", args.categoryId!),
        );
    } else {
      q = ctx.db.query("products");
    }

    const page = await q.take(limit);
    return enrichProducts(ctx, page as any);
  },
});

const SHOP_LIST_LIMIT = 200;

// Filter args: optional brand, tags (product has any), attributes (overlap per dimension).
const shopFilterArgsValidator = {
  brand: v.optional(v.string()),
  tags: v.optional(v.array(v.string())),
  attributes: v.optional(
    v.object({
      petSize: v.optional(v.array(v.string())),
      ageRange: v.optional(v.array(v.string())),
      specialDiet: v.optional(v.array(v.string())),
      material: v.optional(v.string()),
      flavor: v.optional(v.string()),
    }),
  ),
};

type ShopFilterArgs = {
  brand?: string;
  tags?: string[];
  attributes?: {
    petSize?: string[];
    ageRange?: string[];
    specialDiet?: string[];
    material?: string;
    flavor?: string;
  };
};

function productMatchesFilters(
  p: { brand?: string; tags: string[]; attributes?: { petSize?: string[]; ageRange?: string[]; specialDiet?: string[]; material?: string; flavor?: string } },
  filters: ShopFilterArgs,
): boolean {
  if (filters.brand != null && filters.brand !== "" && p.brand !== filters.brand) return false;
  if (filters.tags != null && filters.tags.length > 0) {
    const hasAny = (p.tags ?? []).some((t) => filters.tags!.includes(t));
    if (!hasAny) return false;
  }
  const attrs = filters.attributes;
  if (attrs) {
    if (attrs.petSize != null && attrs.petSize.length > 0) {
      const productVals = p.attributes?.petSize ?? [];
      if (!attrs.petSize.some((v) => productVals.includes(v))) return false;
    }
    if (attrs.ageRange != null && attrs.ageRange.length > 0) {
      const productVals = p.attributes?.ageRange ?? [];
      if (!attrs.ageRange.some((v) => productVals.includes(v))) return false;
    }
    if (attrs.specialDiet != null && attrs.specialDiet.length > 0) {
      const productVals = p.attributes?.specialDiet ?? [];
      if (!attrs.specialDiet.some((v) => productVals.includes(v))) return false;
    }
    if (attrs.material != null && attrs.material !== "") {
      if (p.attributes?.material !== attrs.material) return false;
    }
    if (attrs.flavor != null && attrs.flavor !== "") {
      if (p.attributes?.flavor !== attrs.flavor) return false;
    }
  }
  return true;
}

export const listActive = query({
  args: {
    categoryId: v.optional(v.id("categories")),
    limit: v.optional(v.number()),
    ...shopFilterArgsValidator,
  },
  handler: async (ctx, args) => {
    const limit = Math.min(args.limit ?? SHOP_LIST_LIMIT, 500);
    const filters: ShopFilterArgs = {
      brand: args.brand,
      tags: args.tags,
      attributes: args.attributes,
    };
    let q;
    if (args.categoryId) {
      q = ctx.db
        .query("products")
        .withIndex("by_status_and_category", (idx) =>
          idx.eq("status", "active").eq("categoryId", args.categoryId!),
        );
    } else {
      q = ctx.db
        .query("products")
        .withIndex("by_status", (idx) => idx.eq("status", "active"));
    }
    const hasFilters = filters.brand != null || (filters.tags?.length ?? 0) > 0 || filters.attributes != null;
    const takeCount = hasFilters ? Math.min(2000, limit * 10) : limit;
    const page = await q.take(takeCount);
    const filtered = (page as any[]).filter((p: any) => productMatchesFilters(p, filters)).slice(0, limit);
    return enrichProducts(ctx, filtered as any);
  },
});

export const listByRootCategory = query({
  args: {
    rootCategoryId: v.id("categories"),
    limit: v.optional(v.number()),
    ...shopFilterArgsValidator,
  },
  handler: async (ctx, args) => {
    const limit = Math.min(args.limit ?? SHOP_LIST_LIMIT, 500);
    const filters: ShopFilterArgs = {
      brand: args.brand,
      tags: args.tags,
      attributes: args.attributes,
    };
    const root = await ctx.db.get(args.rootCategoryId);
    if (!root) return [];
    const parentCategorySlug = root.slug;
    const hasFilters =
      filters.brand != null ||
      (filters.tags?.length ?? 0) > 0 ||
      filters.attributes != null;
    const takeCount = hasFilters ? Math.min(2000, limit * 10) : limit;
    const products = await ctx.db
      .query("products")
      .withIndex("by_status_and_parent_slug", (q) =>
        q.eq("status", "active").eq("parentCategorySlug", parentCategorySlug),
      )
      .take(takeCount);
    const filtered = (products as any[]).filter((p: any) => productMatchesFilters(p, filters)).slice(0, limit);
    return enrichProducts(ctx, filtered as any);
  },
});

export const listByTopCategory = query({
  args: {
    topCategorySlug: v.string(),
    petCategorySlug: v.optional(v.string()),
    limit: v.optional(v.number()),
    ...shopFilterArgsValidator,
  },
  handler: async (ctx, args) => {
    const limit = Math.min(args.limit ?? SHOP_LIST_LIMIT, 500);
    const filters: ShopFilterArgs = {
      brand: args.brand,
      tags: args.tags,
      attributes: args.attributes,
    };
    const hasFilters =
      filters.brand != null ||
      (filters.tags?.length ?? 0) > 0 ||
      filters.attributes != null;
    const takeCount = hasFilters ? Math.min(2000, limit * 10) : limit;
    let products;
    if (args.petCategorySlug) {
      products = await ctx.db
        .query("products")
        .withIndex("by_status_and_top_and_parent_slug", (q) =>
          q
            .eq("status", "active")
            .eq("topCategorySlug", args.topCategorySlug)
            .eq("parentCategorySlug", args.petCategorySlug!),
        )
        .take(takeCount);
    } else {
      products = await ctx.db
        .query("products")
        .withIndex("by_status_and_top_category_slug", (q) =>
          q.eq("status", "active").eq("topCategorySlug", args.topCategorySlug),
        )
        .take(takeCount);
    }
    const filteredProducts = (products as any[]).filter((p: any) => productMatchesFilters(p, filters)).slice(0, limit);
    return enrichProducts(ctx, filteredProducts as any);
  },
});

export const listByParentSlug = query({
  args: {
    parentCategorySlug: v.string(),
    limit: v.optional(v.number()),
    ...shopFilterArgsValidator,
  },
  handler: async (ctx, args) => {
    const limit = Math.min(args.limit ?? SHOP_LIST_LIMIT, 500);
    const filters: ShopFilterArgs = {
      brand: args.brand,
      tags: args.tags,
      attributes: args.attributes,
    };
    const hasFilters =
      filters.brand != null ||
      (filters.tags?.length ?? 0) > 0 ||
      filters.attributes != null;
    const takeCount = hasFilters ? Math.min(2000, limit * 10) : limit;
    const products = await ctx.db
      .query("products")
      .withIndex("by_status_and_parent_slug", (q) =>
        q.eq("status", "active").eq("parentCategorySlug", args.parentCategorySlug),
      )
      .take(takeCount);
    const filtered = (products as any[]).filter((p: any) => productMatchesFilters(p, filters)).slice(0, limit);
    return enrichProducts(ctx, filtered as any);
  },
});

export const listByTag = query({
  args: {
    tag: v.string(),
    limit: v.optional(v.number()),
    ...shopFilterArgsValidator,
  },
  handler: async (ctx, args) => {
    const limit = Math.min(args.limit ?? SHOP_LIST_LIMIT, 500);
    const filters: ShopFilterArgs = {
      brand: args.brand,
      tags: args.tags,
      attributes: args.attributes,
    };
    const hasFilters =
      filters.brand != null ||
      (filters.tags?.length ?? 0) > 0 ||
      filters.attributes != null;
    const takeCount = hasFilters ? Math.min(2000, limit * 10) : limit * 5;
    const page = await ctx.db
      .query("products")
      .withIndex("by_status", (q) => q.eq("status", "active"))
      .take(takeCount);
    const filtered = (page as any[])
      .filter((p: any) => (p.tags ?? []).includes(args.tag))
      .filter((p: any) => productMatchesFilters(p, filters))
      .slice(0, limit);
    return enrichProducts(ctx, filtered as any);
  },
});

const RECENTLY_ADDED_LIMIT = 100;
const RECENTLY_ADDED_SCAN_CEILING = 500;
const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;

export const listRecentlyAdded = query({
  args: {
    limit: v.optional(v.number()),
    ...shopFilterArgsValidator,
  },
  handler: async (ctx, args) => {
    const limit = Math.min(args.limit ?? RECENTLY_ADDED_LIMIT, RECENTLY_ADDED_LIMIT);
    const filters: ShopFilterArgs = {
      brand: args.brand,
      tags: args.tags,
      attributes: args.attributes,
    };
    const cutoff = Date.now() - THIRTY_DAYS_MS;
    const collected: any[] = [];
    let scanned = 0;
    for await (const p of ctx.db
      .query("products")
      .withIndex("by_status", (q: any) => q.eq("status", "active"))
      .order("desc")) {
      if (p._creationTime < cutoff) break;
      if (scanned++ >= RECENTLY_ADDED_SCAN_CEILING) break;
      if (productMatchesFilters(p as any, filters)) {
        collected.push(p);
        if (collected.length >= limit) break;
      }
    }
    return enrichProducts(ctx, collected as any);
  },
});

export const listByParentAndChildSlug = query({
  args: {
    parentCategorySlug: v.string(),
    childCategorySlug: v.string(),
    limit: v.optional(v.number()),
    ...shopFilterArgsValidator,
  },
  handler: async (ctx, args) => {
    const limit = Math.min(args.limit ?? SHOP_LIST_LIMIT, 500);
    const filters: ShopFilterArgs = {
      brand: args.brand,
      tags: args.tags,
      attributes: args.attributes,
    };
    const hasFilters =
      filters.brand != null ||
      (filters.tags?.length ?? 0) > 0 ||
      filters.attributes != null;
    const takeCount = hasFilters ? Math.min(2000, limit * 10) : limit;
    const products = await ctx.db
      .query("products")
      .withIndex("by_status_and_parent_and_child_slug", (q) =>
        q
          .eq("status", "active")
          .eq("parentCategorySlug", args.parentCategorySlug)
          .eq("childCategorySlug", args.childCategorySlug),
      )
      .take(takeCount);
    const filtered = (products as any[]).filter((p: any) => productMatchesFilters(p, filters)).slice(0, limit);
    return enrichProducts(ctx, filtered as any);
  },
});

// ─── Filter options (derived from products in scope) ───────────────────────

const FILTER_OPTIONS_PRODUCT_LIMIT = 2000;

async function getProductsInScope(
  ctx: { db: any },
  scope: {
    categoryId?: Id<"categories">;
    rootCategoryId?: Id<"categories">;
    parentCategorySlug?: string;
    childCategorySlug?: string;
    topCategorySlug?: string;
    petCategorySlug?: string;
    tag?: string;
    recentlyAdded?: boolean;
  },
): Promise<Array<{ brand?: string; tags: string[]; attributes?: { petSize?: string[]; ageRange?: string[]; specialDiet?: string[]; material?: string; flavor?: string } }>> {
  // Resolve which scope is used and fetch raw products (no enrichment needed for aggregation).
  if (scope.categoryId !== undefined) {
    return ctx.db
      .query("products")
      .withIndex("by_status_and_category", (q: any) =>
        q.eq("status", "active").eq("categoryId", scope.categoryId),
      )
      .take(FILTER_OPTIONS_PRODUCT_LIMIT);
  }
  if (scope.rootCategoryId !== undefined) {
    const root = await ctx.db.get(scope.rootCategoryId);
    if (!root) return [];
    return ctx.db
      .query("products")
      .withIndex("by_status_and_parent_slug", (q: any) =>
        q.eq("status", "active").eq("parentCategorySlug", root.slug),
      )
      .take(FILTER_OPTIONS_PRODUCT_LIMIT);
  }
  if (scope.parentCategorySlug !== undefined && scope.childCategorySlug !== undefined) {
    return ctx.db
      .query("products")
      .withIndex("by_status_and_parent_and_child_slug", (q: any) =>
        q
          .eq("status", "active")
          .eq("parentCategorySlug", scope.parentCategorySlug)
          .eq("childCategorySlug", scope.childCategorySlug),
      )
      .take(FILTER_OPTIONS_PRODUCT_LIMIT);
  }
  if (scope.parentCategorySlug !== undefined) {
    return ctx.db
      .query("products")
      .withIndex("by_status_and_parent_slug", (q: any) =>
        q.eq("status", "active").eq("parentCategorySlug", scope.parentCategorySlug),
      )
      .take(FILTER_OPTIONS_PRODUCT_LIMIT);
  }
  if (scope.topCategorySlug !== undefined) {
    if (scope.petCategorySlug) {
      return ctx.db
        .query("products")
        .withIndex("by_status_and_top_and_parent_slug", (q: any) =>
          q
            .eq("status", "active")
            .eq("topCategorySlug", scope.topCategorySlug)
            .eq("parentCategorySlug", scope.petCategorySlug),
        )
        .take(FILTER_OPTIONS_PRODUCT_LIMIT);
    }
    return ctx.db
      .query("products")
      .withIndex("by_status_and_top_category_slug", (q: any) =>
        q.eq("status", "active").eq("topCategorySlug", scope.topCategorySlug),
      )
      .take(FILTER_OPTIONS_PRODUCT_LIMIT);
  }
  if (scope.tag !== undefined) {
    const all = await ctx.db
      .query("products")
      .withIndex("by_status", (q: any) => q.eq("status", "active"))
      .take(FILTER_OPTIONS_PRODUCT_LIMIT);
    return all.filter((p: any) => (p.tags ?? []).includes(scope.tag!));
  }
  if (scope.recentlyAdded === true) {
    const cutoff = Date.now() - THIRTY_DAYS_MS;
    const results: any[] = [];
    for await (const p of ctx.db
      .query("products")
      .withIndex("by_status", (q: any) => q.eq("status", "active"))
      .order("desc")) {
      if (p._creationTime < cutoff) break;
      results.push(p);
      if (results.length >= FILTER_OPTIONS_PRODUCT_LIMIT) break;
    }
    return results;
  }
  // No scope: entire catalog
  return ctx.db
    .query("products")
    .withIndex("by_status", (q: any) => q.eq("status", "active"))
    .take(FILTER_OPTIONS_PRODUCT_LIMIT);
}

function aggregateFilterOptions(
  products: Array<{ brand?: string; tags: string[]; attributes?: { petSize?: string[]; ageRange?: string[]; specialDiet?: string[]; material?: string; flavor?: string } }>,
): {
  brands: string[];
  tags: string[];
  attributes: { petSize: string[]; ageRange: string[]; specialDiet: string[]; material: string[]; flavor: string[] };
} {
  const brandsSet = new Set<string>();
  const tagsSet = new Set<string>();
  const petSizeSet = new Set<string>();
  const ageRangeSet = new Set<string>();
  const specialDietSet = new Set<string>();
  const materialSet = new Set<string>();
  const flavorSet = new Set<string>();

  for (const p of products) {
    if (p.brand != null && p.brand.trim() !== "") brandsSet.add(p.brand);
    for (const t of p.tags ?? []) if (t != null && String(t).trim() !== "") tagsSet.add(String(t));
    const attrs = p.attributes;
    if (attrs) {
      for (const v of attrs.petSize ?? []) if (v != null && String(v).trim() !== "") petSizeSet.add(String(v));
      for (const v of attrs.ageRange ?? []) if (v != null && String(v).trim() !== "") ageRangeSet.add(String(v));
      for (const v of attrs.specialDiet ?? []) if (v != null && String(v).trim() !== "") specialDietSet.add(String(v));
      if (attrs.material != null && String(attrs.material).trim() !== "") materialSet.add(String(attrs.material));
      if (attrs.flavor != null && String(attrs.flavor).trim() !== "") flavorSet.add(String(attrs.flavor));
    }
  }

  const sort = (a: string, b: string) => a.localeCompare(b, "en");
  return {
    brands: [...brandsSet].sort(sort),
    tags: [...tagsSet].sort(sort),
    attributes: {
      petSize: [...petSizeSet].sort(sort),
      ageRange: [...ageRangeSet].sort(sort),
      specialDiet: [...specialDietSet].sort(sort),
      material: [...materialSet].sort(sort),
      flavor: [...flavorSet].sort(sort),
    },
  };
}

export const getFilterOptions = query({
  args: {
    categoryId: v.optional(v.id("categories")),
    rootCategoryId: v.optional(v.id("categories")),
    parentCategorySlug: v.optional(v.string()),
    childCategorySlug: v.optional(v.string()),
    topCategorySlug: v.optional(v.string()),
    petCategorySlug: v.optional(v.string()),
    tag: v.optional(v.string()),
    recentlyAdded: v.optional(v.boolean()),
  },
  handler: async (ctx, args) => {
    const products = await getProductsInScope(ctx, args);
    const aggregated = aggregateFilterOptions(products as any);
    return aggregated;
  },
});

export const getBySlug = query({
  args: { slug: v.string() },
  handler: async (ctx, { slug }) => {
    const product = await ctx.db
      .query("products")
      .withIndex("by_slug", (q) => q.eq("slug", slug))
      .unique();

    if (!product || product.status !== "active") return null;

    return getProductWithRelations(ctx, product._id);
  },
});

export const getById = internalQuery({
  args: { id: v.id("products") },
  handler: async (ctx, { id }) => {
    return getProductWithRelations(ctx, id);
  },
});

export const getByIdForAdmin = query({
  args: { id: v.id("products") },
  handler: async (ctx, { id }) => {
    await Users.requireAdmin(ctx);
    const product = await ctx.db.get(id);
    if (!product) return null;

    const [imagesRaw, variants, category] = await Promise.all([
      ctx.db
        .query("productImages")
        .withIndex("by_product", (q) => q.eq("productId", id))
        .collect(),
      // Admin sees ALL variants (active and inactive)
      ctx.db
        .query("productVariants")
        .withIndex("by_product", (q) => q.eq("productId", id))
        .collect(),
      ctx.db.get(product.categoryId),
    ]);
    const images = imagesRaw.sort((a, b) => a.position - b.position);

    return { ...product, images, variants, category };
  },
});

/**
 * One-time migration: backfill parentCategorySlug, childCategorySlug, topCategorySlug
 * from categories. Run once after deploying Phase 1 schema.
 * Assumes every product's category has parentId set.
 */
export const backfillProductCategorySlugs = internalMutation({
  args: {},
  handler: async (ctx) => {
    const products = await ctx.db.query("products").collect();
    let patched = 0;
    let skipped = 0;
    for (const product of products) {
      const category = await ctx.db.get(product.categoryId);
      if (!category) {
        skipped += 1;
        continue;
      }
      if (!category.parentId) {
        skipped += 1;
        continue;
      }
      const parent = await ctx.db.get(category.parentId);
      if (!parent) {
        skipped += 1;
        continue;
      }
      const parentCategorySlug = parent.slug;
      const childCategorySlug = category.slug;
      const topCategorySlug =
        category.topCategorySlug !== undefined && category.topCategorySlug !== null
          ? category.topCategorySlug
          : undefined;
      await ctx.db.patch(product._id, {
        parentCategorySlug,
        childCategorySlug,
        ...(topCategorySlug !== undefined && { topCategorySlug }),
      });
      patched += 1;
    }
    return { patched, skipped };
  },
});

const productStatusValidator = v.union(
  v.literal("active"),
  v.literal("draft"),
  v.literal("archived"),
);

export const search = query({
  args: {
    query: v.string(),
    status: v.optional(productStatusValidator),
    categoryId: v.optional(v.id("categories")),
    brand: v.optional(v.string()),
    limit: v.optional(v.number()),
  },
  handler: async (ctx, args) => {
    const trimmed = args.query.trim();
    if (!trimmed) return [];

    let searchFilter = (q: any) => {
      if (args.status !== undefined) q = q.eq("status", args.status);
      if (args.categoryId !== undefined)
        q = q.eq("categoryId", args.categoryId);
      if (args.brand !== undefined) q = q.eq("brand", args.brand);
      return q.search("name", trimmed);
    };

    const limit = args.limit ?? 24;
    const results = await ctx.db
      .query("products")
      .withSearchIndex("search_products", searchFilter)
      .take(limit);

    return enrichProducts(ctx, results as any);
  },
});

export const searchTypeahead = query({
  args: {
    query: v.string(),
    parentCategorySlug: v.optional(v.string()),
    limit: v.optional(v.number()),
  },
  handler: async (ctx, args) => {
    const trimmed = args.query.trim();
    if (trimmed.length < 3) return [];

    const limit = Math.min(args.limit ?? 8, 20);

    const searchFilter = (q: any) => {
      q = q.eq("status", "active");
      if (args.parentCategorySlug !== undefined) {
        q = q.eq("parentCategorySlug", args.parentCategorySlug);
      }
      return q.search("name", trimmed);
    };

    const results = await ctx.db
      .query("products")
      .withSearchIndex("search_products", searchFilter)
      .take(limit);

    return Promise.all(
      results.map(async (product) => {
        const firstImage = await ctx.db
          .query("productImages")
          .withIndex("by_product", (q) => q.eq("productId", product._id))
          .first();

        const variants = await ctx.db
          .query("productVariants")
          .withIndex("by_product_and_active", (q) =>
            q.eq("productId", product._id).eq("isActive", true),
          )
          .collect();

        const minPriceVariant =
          variants.length > 0
            ? variants.reduce((min, v) => (v.price < min.price ? v : min))
            : null;

        return {
          _id: product._id,
          name: product.name,
          slug: product.slug,
          parentCategorySlug: product.parentCategorySlug,
          childCategorySlug: product.childCategorySlug,
          brand: product.brand,
          imageUrl: firstImage?.url,
          imageAlt: firstImage?.alt,
          minPrice: minPriceVariant?.price ?? 0,
          compareAtPrice: minPriceVariant?.compareAtPrice,
          averageRating: product.averageRating,
          reviewCount: product.reviewCount,
        };
      }),
    );
  },
});

export const create = mutation({
  args: {
    name: v.string(),
    slug: v.string(),
    description: v.optional(v.string()),
    shortDescription: v.optional(v.string()),
    status: v.union(
      v.literal("active"),
      v.literal("draft"),
      v.literal("archived"),
    ),
    categoryId: v.id("categories"),
    brand: v.optional(v.string()),
    tags: v.array(v.string()),
    attributes: v.optional(
      v.object({
        petSize: v.optional(v.array(v.string())),
        ageRange: v.optional(v.array(v.string())),
        specialDiet: v.optional(v.array(v.string())),
        material: v.optional(v.string()),
        flavor: v.optional(v.string()),
      }),
    ),
    seoTitle: v.optional(v.string()),
    seoDescription: v.optional(v.string()),
    canonicalSlug: v.optional(v.string()),
  },
  handler: async (ctx, args) => {
    await Users.requireAdmin(ctx);
    const category = await ctx.db.get(args.categoryId) as Doc<"categories"> | null;
    if (!category) throw new Error("Category not found");
    const parentCategorySlug = category.parentId
      ? (await ctx.db.get(category.parentId) as Doc<"categories"> | null)?.slug ?? category.slug
      : category.slug;
    const childCategorySlug = category.slug;
    const topCategorySlug = category.topCategorySlug ?? undefined;
    return await ctx.db.insert("products", {
      ...args,
      parentCategorySlug,
      childCategorySlug,
      ...(topCategorySlug !== undefined && { topCategorySlug }),
      createdAt: Date.now(),
      updatedAt: Date.now(),
    });
  },
});

export const update = mutation({
  args: {
    id: v.id("products"),
    name: v.optional(v.string()),
    slug: v.optional(v.string()),
    description: v.optional(v.string()),
    shortDescription: v.optional(v.string()),
    status: v.optional(
      v.union(
        v.literal("active"),
        v.literal("draft"),
        v.literal("archived"),
      ),
    ),
    categoryId: v.optional(v.id("categories")),
    brand: v.optional(v.string()),
    tags: v.optional(v.array(v.string())),
    attributes: v.optional(
      v.object({
        petSize: v.optional(v.array(v.string())),
        ageRange: v.optional(v.array(v.string())),
        specialDiet: v.optional(v.array(v.string())),
        material: v.optional(v.string()),
        flavor: v.optional(v.string()),
      }),
    ),
    seoTitle: v.optional(v.string()),
    seoDescription: v.optional(v.string()),
    canonicalSlug: v.optional(v.string()),
  },
  handler: async (ctx, { id, ...updates }) => {
    await Users.requireAdmin(ctx);
    const existing = await ctx.db.get(id);
    if (!existing) throw new Error("Product not found");

    const fields: Record<string, any> = {};
    for (const [key, value] of Object.entries(updates)) {
      if (value !== undefined) fields[key] = value;
    }

    const categoryId = fields.categoryId ?? existing.categoryId;
    const category = await ctx.db.get(categoryId) as Doc<"categories"> | null;
    if (category) {
      const parentCategorySlug = category.parentId
        ? (await ctx.db.get(category.parentId) as Doc<"categories"> | null)?.slug ?? category.slug
        : category.slug;
      fields.parentCategorySlug = parentCategorySlug;
      fields.childCategorySlug = category.slug;
      if (category.topCategorySlug !== undefined && category.topCategorySlug !== null) {
        fields.topCategorySlug = category.topCategorySlug;
      }
    }

    fields.updatedAt = Date.now();

    await ctx.db.patch(id, fields);
    return id;
  },
});

export const archive = mutation({
  args: { id: v.id("products") },
  handler: async (ctx, { id }) => {
    await Users.requireAdmin(ctx);
    const existing = await ctx.db.get(id);
    if (!existing) throw new Error("Product not found");
    await ctx.db.patch(id, { status: "archived" });
    return id;
  },
});

// ─── Product images ───────────────────────────────────────────────────────

export const addImage = mutation({
  args: {
    productId: v.id("products"),
    url: v.string(),
    alt: v.optional(v.string()),
    position: v.number(),
  },
  handler: async (ctx, args) => {
    await Users.requireAdmin(ctx);
    const product = await ctx.db.get(args.productId);
    if (!product) throw new Error("Product not found");
    return await ctx.db.insert("productImages", {
      productId: args.productId,
      url: args.url,
      alt: args.alt,
      position: args.position,
    });
  },
});

export const deleteImage = mutation({
  args: { id: v.id("productImages") },
  handler: async (ctx, { id }) => {
    await Users.requireAdmin(ctx);
    const image = await ctx.db.get(id);
    if (!image) throw new Error("Image not found");
    await ctx.db.delete(id);
    return id;
  },
});

export const reorderImages = mutation({
  args: {
    updates: v.array(
      v.object({
        id: v.id("productImages"),
        position: v.number(),
      }),
    ),
  },
  handler: async (ctx, { updates }) => {
    await Users.requireAdmin(ctx);
    for (const { id, position } of updates) {
      const image = await ctx.db.get(id);
      if (!image) throw new Error(`Image not found: ${id}`);
      await ctx.db.patch(id, { position });
    }
  },
});

// ─── Product variants ──────────────────────────────────────────────────────

const variantAttributesValidator = v.optional(
  v.object({
    size: v.optional(v.string()),
    flavor: v.optional(v.string()),
    color: v.optional(v.string()),
  }),
);

export const addVariant = mutation({
  args: {
    productId: v.id("products"),
    name: v.string(),
    sku: v.string(),
    price: v.number(),
    compareAtPrice: v.optional(v.number()),
    stockQuantity: v.number(),
    attributes: variantAttributesValidator,
    isActive: v.boolean(),
    weight: v.optional(v.number()),
    weightUnit: v.optional(
      v.union(
        v.literal("g"),
        v.literal("kg"),
        v.literal("lb"),
        v.literal("oz"),
      ),
    ),
    length: v.optional(v.number()),
    width: v.optional(v.number()),
    height: v.optional(v.number()),
    dimensionUnit: v.optional(v.union(v.literal("cm"), v.literal("in"))),
  },
  handler: async (ctx, args) => {
    await Users.requireAdmin(ctx);
    const product = await ctx.db.get(args.productId);
    if (!product) throw new Error("Product not found");
    const existingSku = await ctx.db
      .query("productVariants")
      .withIndex("by_sku", (q) => q.eq("sku", args.sku))
      .unique();
    if (existingSku) throw new Error(`SKU "${args.sku}" is already in use`);
    return await ctx.db.insert("productVariants", {
      productId: args.productId,
      name: args.name,
      sku: args.sku,
      price: args.price,
      compareAtPrice: args.compareAtPrice,
      stockQuantity: args.stockQuantity,
      attributes: args.attributes,
      isActive: args.isActive,
      weight: args.weight ?? 0,
      weightUnit: args.weightUnit ?? "g",
      ...(args.length !== undefined && { length: args.length }),
      ...(args.width !== undefined && { width: args.width }),
      ...(args.height !== undefined && { height: args.height }),
      ...(args.dimensionUnit !== undefined && { dimensionUnit: args.dimensionUnit }),
    });
  },
});

export const updateVariant = mutation({
  args: {
    id: v.id("productVariants"),
    name: v.optional(v.string()),
    sku: v.optional(v.string()),
    price: v.optional(v.number()),
    compareAtPrice: v.optional(v.number()),
    stockQuantity: v.optional(v.number()),
    isActive: v.optional(v.boolean()),
    weight: v.optional(v.number()),
    weightUnit: v.optional(
      v.union(
        v.literal("g"),
        v.literal("kg"),
        v.literal("lb"),
        v.literal("oz"),
      ),
    ),
    attributes: variantAttributesValidator,
    length: v.optional(v.number()),
    width: v.optional(v.number()),
    height: v.optional(v.number()),
    dimensionUnit: v.optional(v.union(v.literal("cm"), v.literal("in"))),
  },
  handler: async (ctx, { id, sku, ...updates }) => {
    await Users.requireAdmin(ctx);
    const variant = await ctx.db.get(id);
    if (!variant) throw new Error("Variant not found");

    // Enforce SKU uniqueness if sku is being changed
    if (sku !== undefined && sku !== variant.sku) {
      const existing = await ctx.db
        .query("productVariants")
        .withIndex("by_sku", (q) => q.eq("sku", sku))
        .unique();
      if (existing && existing._id !== id) {
        throw new Error(`SKU "${sku}" is already in use by another variant`);
      }
    }

    const fields: Record<string, unknown> = {};
    if (sku !== undefined) fields.sku = sku;
    for (const [key, value] of Object.entries(updates)) {
      if (value !== undefined) fields[key] = value;
    }
    if (Object.keys(fields).length > 0) {
      await ctx.db.patch(id, fields);
    }
    return id;
  },
});

export const deleteVariant = mutation({
  args: { id: v.id("productVariants") },
  handler: async (ctx, { id }) => {
    await Users.requireAdmin(ctx);
    const variant = await ctx.db.get(id);
    if (!variant) throw new Error("Variant not found");
    const orderItemsWithVariant = await ctx.db
      .query("orderItems")
      .filter((q) => q.eq(q.field("variantId"), id))
      .collect();
    if (orderItemsWithVariant.length > 0) {
      await ctx.db.patch(id, { isActive: false });
    } else {
      await ctx.db.delete(id);
    }
    return id;
  },
});
