import { createHash } from "node:crypto"
import { Modules, ProductStatus, ContainerRegistrationKeys } from "@medusajs/framework/utils"
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3"
import {
  batchProductVariantsWorkflow,
  createProductsWorkflow,
  updateProductsWorkflow,
} from "@medusajs/medusa/core-flows"

type SanityVariationAttribute = {
  name?: string | null
  slug?: string | null
  option?: string | null
}

type SanityVariation = {
  _key?: string | null
  sku?: string | null
  price?: string | number | null
  regular_price?: string | number | null
  sale_price?: string | number | null
  stock_status?: string | null
  stock_quantity?: number | null
  weight?: number | string | null
  length?: number | string | null
  width?: number | string | null
  height?: number | string | null
  image?: {
    assetUrl?: string | null
    src?: string | null
    url?: string | null
  } | null
  attributes?: SanityVariationAttribute[] | null
}

type SanityPersonalizationField = {
  label?: string | null
  key?: string | null
  inputType?: string | null
  useThemeOptions?: boolean | null
  options?: Array<string | { label?: string | null; priceCents?: number | null } | null> | null
  helperText?: string | null
  helperImageUrl?: string | null
  helperImageLabel?: string | null
  showLabel?: boolean | null
  placeholder?: string | null
  defaultValue?: string | null
  maxLength?: number | null
  required?: boolean | null
}

type SanityVariationAxis = {
  label?: string | null
  key?: string | null
  kind?: string | null
  values?: Array<string | null> | null
}

type SanityVariantSelection = {
  axisKey?: string | null
  value?: string | null
}

type SanityVariantDefinition = {
  isEnabled?: boolean | null
  title?: string | null
  sku?: string | null
  price?: string | number | null
  regular_price?: string | number | null
  sale_price?: string | number | null
  stock_status?: string | null
  stock_quantity?: number | null
  weight?: number | string | null
  length?: number | string | null
  width?: number | string | null
  height?: number | string | null
  image?: {
    assetUrl?: string | null
    src?: string | null
    url?: string | null
  } | null
  selections?: SanityVariantSelection[] | null
}

export type SanityProductSyncPayload = {
  _id: string
  title?: string | null
  handle?: string | null
  status?: string | null
  isActive?: boolean | null
  shortDescription?: string | null
  description?: string | null
  careText?: string | null
  priceCents?: number | null
  regularPriceCents?: number | null
  salePriceCents?: number | null
  mainImageAssetUrl?: string | null
  imageUrl?: string | null
  gallery?: Array<{
    _key?: string | null
    assetUrl?: string | null
    url?: string | null
  }> | null
  themeOptions?: Array<string | null> | null
  stockStatus?: string | null
  stockQuantity?: number | null
  weight?: number | string | null
  length?: number | string | null
  width?: number | string | null
  height?: number | string | null
  variations?: SanityVariation[] | null
  variationAxes?: SanityVariationAxis[] | null
  variantDefinitions?: SanityVariantDefinition[] | null
  personalizationFields?: SanityPersonalizationField[] | null
}

type ContainerLike = {
  resolve: <T = unknown>(key: string) => T
}

type ExistingProductRecord = {
  id: string
  handle?: string | null
  metadata?: {
    sanity_document_id?: string | null
  } | null
}

type ExistingVariantRecord = {
  id: string
  sku?: string | null
  product?: { id?: string | null } | null
}

type BuiltVariantAxis = {
  key: string
  title: string
  kind: string
  values: string[]
  order: number
}

function normalizePersonalizationKey(value: string) {
  return value
    .normalize("NFD")
    .replace(/[\u0300-\u036f]/g, "")
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/^-+|-+$/g, "")
}

function readSanityEnv() {
  return {
    projectId:
      process.env.SANITY_PROJECT_ID ||
      process.env.SANITY_STUDIO_PROJECT_ID ||
      process.env.NEXT_PUBLIC_SANITY_PROJECT_ID ||
      "",
    dataset:
      process.env.SANITY_DATASET ||
      process.env.SANITY_STUDIO_DATASET ||
      process.env.NEXT_PUBLIC_SANITY_DATASET ||
      "production",
    apiVersion: process.env.SANITY_API_VERSION || process.env.NEXT_PUBLIC_SANITY_API_VERSION || "2025-01-01",
    token: process.env.SANITY_API_READ_TOKEN || process.env.SANITY_API_WRITE_TOKEN || "",
  }
}

function readS3Env() {
  return {
    region: process.env.S3_REGION || "",
    bucket: process.env.S3_BUCKET || "",
    fileUrl: process.env.S3_FILE_URL || "",
    prefix: process.env.S3_PREFIX || "uploads/",
    accessKeyId: process.env.S3_ACCESS_KEY_ID || "",
    secretAccessKey: process.env.S3_SECRET_ACCESS_KEY || "",
  }
}

function normalizeKey(value: string) {
  return value
    .normalize("NFD")
    .replace(/[\u0300-\u036f]/g, "")
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "")
}

function toAmount(value: unknown) {
  if (value === null || value === undefined || value === "") return null

  if (typeof value === "number" && Number.isFinite(value)) {
    return value > 999 ? Math.round(value) : Math.round(value * 100)
  }

  if (typeof value === "string") {
    const text = value.trim()
    if (!text) return null
    if (/^\d+$/.test(text)) {
      const parsedInt = Number(text)
      return parsedInt > 999 ? parsedInt : parsedInt * 100
    }

    const parsed = Number(text.replace(",", "."))
    if (Number.isFinite(parsed)) {
      return Math.round(parsed * 100)
    }
  }

  return null
}

function pickAmount(...values: unknown[]) {
  for (const value of values) {
    const amount = toAmount(value)
    if (amount !== null) {
      return amount
    }
  }

  return 0
}

function toFiniteNumber(value: unknown) {
  if (typeof value === "number") {
    return Number.isFinite(value) ? value : undefined
  }

  if (typeof value === "string") {
    const text = value.trim()
    if (!text) return undefined

    const parsed = Number(text.replace(",", "."))
    return Number.isFinite(parsed) ? parsed : undefined
  }

  return undefined
}

function sanitizeSegment(value: string) {
  return value
    .toLowerCase()
    .replace(/[^a-z0-9-_]+/g, "-")
    .replace(/^-+|-+$/g, "")
    .slice(0, 60) || "item"
}

function getExtensionFromMime(contentType: string) {
  const value = contentType.toLowerCase()
  if (value.includes("png")) return "png"
  if (value.includes("jpeg") || value.includes("jpg")) return "jpg"
  if (value.includes("webp")) return "webp"
  if (value.includes("gif")) return "gif"
  return "bin"
}

function getExtensionFromUrl(url: string) {
  const lower = url.toLowerCase()
  if (lower.includes(".png")) return "png"
  if (lower.includes(".jpg") || lower.includes(".jpeg")) return "jpg"
  if (lower.includes(".webp")) return "webp"
  if (lower.includes(".gif")) return "gif"
  return ""
}

function joinUrl(base: string, key: string) {
  const cleanBase = base.replace(/\/+$/, "")
  return `${cleanBase}/${key.split("/").map(encodeURIComponent).join("/")}`
}

function buildImageList(product: SanityProductSyncPayload) {
  const urls = [
    getEffectiveMainImageUrl(product),
    ...((product.gallery || []).map((item) => item?.url || item?.assetUrl || "").filter(Boolean)),
  ].filter(Boolean)

  return [...new Set(urls)].map((url) => ({ url }))
}

function getEffectiveMainImageUrl(product: SanityProductSyncPayload) {
  return (
    (product.imageUrl || "").trim() ||
    (product.mainImageAssetUrl || "").trim() ||
    (product.gallery || [])
      .map((item) => ((item?.url || "").trim() || (item?.assetUrl || "").trim()))
      .find(Boolean) ||
    ""
  )
}

async function mutateSanityDocument(
  documentId: string,
  set: Record<string, string | null>,
  unset: string[] = [],
) {
  const sanityEnv = readSanityEnv()
  const token = process.env.SANITY_API_WRITE_TOKEN || ""

  if (!sanityEnv.projectId || !token || (Object.keys(set).length === 0 && unset.length === 0)) {
    return false
  }

  const url = `https://${sanityEnv.projectId}.api.sanity.io/v${sanityEnv.apiVersion}/data/mutate/${sanityEnv.dataset}`
  const response = await fetch(url, {
    method: "POST",
    headers: {
      "content-type": "application/json",
      authorization: `Bearer ${token}`,
    },
    body: JSON.stringify({
      mutations: [
        {
          patch: {
            id: documentId,
            set,
            unset,
          },
        },
      ],
    }),
  })

  if (!response.ok) {
    throw new Error(`Sanity mutate failed (${response.status}).`)
  }

  return true
}

async function migrateSanityProductImagesToS3(product: SanityProductSyncPayload) {
  const s3Env = readS3Env()

  if (!s3Env.region || !s3Env.bucket || !s3Env.fileUrl || !s3Env.accessKeyId || !s3Env.secretAccessKey) {
    return product
  }

  const s3 = new S3Client({
    region: s3Env.region,
    credentials: {
      accessKeyId: s3Env.accessKeyId,
      secretAccessKey: s3Env.secretAccessKey,
    },
  })

  const uploadedCache = new Map<string, string>()
  const keyRoot = `sanity-products/${sanitizeSegment(product.handle || product.title || product._id)}-${sanitizeSegment(product._id)}`

  const migrateUrl = async (sourceUrl: string, keyPathPrefix: string) => {
    const normalizedUrl = sourceUrl.trim()
    if (!normalizedUrl) return null
    if (normalizedUrl.startsWith(s3Env.fileUrl)) return normalizedUrl

    const cached = uploadedCache.get(normalizedUrl)
    if (cached) return cached

    let response: Response
    try {
      response = await fetch(normalizedUrl)
    } catch (error) {
      console.warn(`[sanity-sync] Image download failed for ${normalizedUrl}:`, error)
      return null
    }

    if (!response.ok) {
      console.warn(`[sanity-sync] Image download failed (${response.status}) for ${normalizedUrl}`)
      return null
    }

    const bytes = new Uint8Array(await response.arrayBuffer())
    const contentType = response.headers.get("content-type") || "application/octet-stream"
    const ext = getExtensionFromUrl(normalizedUrl) || getExtensionFromMime(contentType)
    const digest = createHash("sha1").update(bytes).digest("hex").slice(0, 16)
    const key = `${s3Env.prefix.replace(/\/?$/, "/")}${keyPathPrefix}/${digest}.${ext}`

    await s3.send(
      new PutObjectCommand({
        Bucket: s3Env.bucket,
        Key: key,
        Body: bytes,
        ContentType: contentType,
        CacheControl: "public, max-age=31536000",
      })
    )

    const migratedUrl = joinUrl(s3Env.fileUrl, key)
    uploadedCache.set(normalizedUrl, migratedUrl)
    return migratedUrl
  }

  const mainSourceUrl =
    (product.imageUrl || "").trim() ||
    (product.mainImageAssetUrl || "").trim() ||
    ""
  const nextImageUrl = mainSourceUrl ? await migrateUrl(mainSourceUrl, `${keyRoot}/main`) : product.imageUrl || null

  const nextGallery = await Promise.all(
    (product.gallery || []).map(async (item, index) => {
      const sourceUrl = (item?.url || "").trim() || (item?.assetUrl || "").trim()
      if (!sourceUrl) return item

      const migratedUrl = await migrateUrl(sourceUrl, `${keyRoot}/gallery/${String(index + 1).padStart(2, "0")}`)
      return {
        ...item,
        url: migratedUrl || item?.url || item?.assetUrl || null,
      }
    })
  )

  const nextVariations = await Promise.all(
    (product.variations || []).map(async (variation, index) => {
      const sourceUrl = (variation.image?.assetUrl || "").trim() || (variation.image?.url || "").trim() || (variation.image?.src || "").trim()
      if (!sourceUrl) return variation

      const migratedUrl = await migrateUrl(sourceUrl, `${keyRoot}/variants/${String(index + 1).padStart(2, "0")}`)
      return {
        ...variation,
        image: {
          ...variation.image,
          assetUrl: migratedUrl || variation.image?.assetUrl || variation.image?.url || variation.image?.src || null,
          url: migratedUrl || variation.image?.url || variation.image?.src || null,
          src: migratedUrl || variation.image?.src || variation.image?.url || null,
        },
      }
    })
  )

  const patchSet: Record<string, string | null> = {}
  const patchUnset: string[] = []
  if (nextImageUrl && nextImageUrl !== (product.imageUrl || "").trim()) {
    patchSet.imageUrl = nextImageUrl
  }

  for (const item of nextGallery) {
    const key = (item?._key || "").trim()
    const url = (item?.url || "").trim()
    if (!key || !url) continue
    patchSet[`gallery[_key=="${key}"].url`] = url
    patchUnset.push(`gallery[_key=="${key}"].image`)
  }

  if (Object.keys(patchSet).length > 0 || patchUnset.length > 0) {
    await mutateSanityDocument(product._id, patchSet, patchUnset)
  }

  return {
    ...product,
    imageUrl: nextImageUrl,
    gallery: nextGallery,
    variations: nextVariations,
  }
}

type SyncSanityProductToMedusaOptions = {
  allowSanityWriteback?: boolean
}

function buildPersonalizationFields(product: SanityProductSyncPayload) {
  const themeOptions = (product.themeOptions || []).map((value) => (value || "").trim()).filter(Boolean)

  const normalizePersonalizationOption = (value: string | { label?: string | null; priceCents?: number | null } | null) => {
    if (typeof value === "string") {
      const label = value.trim()
      return label ? { label, priceCents: 0 } : null
    }

    if (!value || typeof value !== "object") {
      return null
    }

    const label = (value.label || "").trim()
    const priceCents = typeof value.priceCents === "number" && Number.isFinite(value.priceCents) ? Math.max(0, Math.round(value.priceCents)) : 0

    return label ? { label, priceCents } : null
  }

  return (product.personalizationFields || [])
    .map((field, index) => {
      const label = (field?.label || "").trim()
      const key = normalizePersonalizationKey((field?.key || "").trim() || label)
      const inputType = ["choices", "select", "radio", "text", "textarea", "email", "tel", "number", "date", "image", "color"].includes(
        field?.inputType || "",
      )
        ? field?.inputType
        : "choices"
      const options = (field?.useThemeOptions ? themeOptions.map((value) => ({ label: value, priceCents: 0 })) : field?.options || [])
        .map((value) => normalizePersonalizationOption(value))
        .filter((value): value is { label: string; priceCents: number } => Boolean(value))

      if (!label || !key) return null
      if (["choices", "select", "radio"].includes(inputType || "") && options.length === 0) return null

      return {
        key,
        label,
        inputType,
        useThemeOptions: field?.useThemeOptions === true,
        options,
        helperText: (field?.helperText || "").trim() || null,
        helperImageUrl: (field?.helperImageUrl || "").trim() || null,
        helperImageLabel: (field?.helperImageLabel || "").trim() || null,
        showLabel: field?.showLabel !== false,
        placeholder: (field?.placeholder || "").trim() || null,
        defaultValue: (field?.defaultValue || "").trim() || null,
        maxLength: typeof field?.maxLength === "number" && Number.isFinite(field.maxLength) ? field.maxLength : null,
        required: field?.required === true,
        order: index,
      }
    })
    .filter(Boolean)
}

function buildConfiguredVariationAxes(product: SanityProductSyncPayload): BuiltVariantAxis[] {
  return (product.variationAxes || [])
    .map((axis, index) => {
      const title = (axis?.label || "").trim()
      const key = normalizePersonalizationKey((axis?.key || "").trim() || title)
      const values = (axis?.values || []).map((value) => (value || "").trim()).filter(Boolean)

      if (!title || !key || values.length === 0) return null

      return {
        key,
        title,
        kind: (axis?.kind || "other").trim() || "other",
        values,
        order: index,
      } satisfies BuiltVariantAxis
    })
    .filter((value): value is BuiltVariantAxis => Boolean(value))
}

function buildConfiguredProductOptionsAndVariants(product: SanityProductSyncPayload) {
  const axes = buildConfiguredVariationAxes(product)
  const sourceVariants = (product.variantDefinitions || []).filter((variant) => variant?.isEnabled !== false)

  if (!axes.length) {
    return null
  }

  const options = axes.map((axis) => ({
    title: axis.title,
    values: axis.values,
  }))

  const variants = sourceVariants.length
    ? sourceVariants
        .map((variant, index) => {
          const selectionMap = new Map(
            (variant?.selections || [])
              .map((selection) => [normalizePersonalizationKey((selection?.axisKey || "").trim()), (selection?.value || "").trim()] as const)
              .filter((entry) => entry[0] && entry[1]),
          )

          const selectedOptions = Object.fromEntries(
            axes.map((axis) => [axis.title, selectionMap.get(axis.key) || axis.values[0] || "Standard"]),
          )

          return {
            title: (variant?.title || "").trim() || Object.values(selectedOptions).join(" / ") || `Standard ${index + 1}`,
            sku:
              (variant?.sku || "").trim() ||
              `SANITY-${createHash("sha1").update(`${product.handle || product._id}-${index}`).digest("hex").slice(0, 12)}`,
            options: selectedOptions,
            manage_inventory: false,
            prices: [
              {
                amount: pickAmount(variant?.sale_price, variant?.price, variant?.regular_price, product.salePriceCents, product.priceCents),
                currency_code: "eur",
              },
            ],
            weight: toFiniteNumber(variant?.weight),
            length: toFiniteNumber(variant?.length),
            width: toFiniteNumber(variant?.width),
            height: toFiniteNumber(variant?.height),
            thumbnail: variant?.image?.assetUrl || variant?.image?.url || variant?.image?.src || getEffectiveMainImageUrl(product) || undefined,
          }
        })
        .filter(Boolean)
    : buildCartesianOptionVariants(product, axes)

  if (!variants.length) {
    return null
  }

  return {
    options,
    variants,
    metadataAxes: axes.map((axis) => ({
      key: axis.key,
      label: axis.title,
      kind: axis.kind,
      values: axis.values,
      order: axis.order,
    })),
  }
}

function buildCartesianOptionVariants(product: SanityProductSyncPayload, axes: BuiltVariantAxis[]) {
  const combinations = axes.reduce<Array<Record<string, string>>>(
    (acc, axis) => {
      const next: Array<Record<string, string>> = []

      for (const current of acc) {
        for (const value of axis.values) {
          next.push({
            ...current,
            [axis.title]: value,
          })
        }
      }

      return next
    },
    [{}],
  )

  return combinations.map((selectedOptions, index) => {
    const serialized = JSON.stringify(selectedOptions)

    return {
      title: Object.values(selectedOptions).join(" / ") || `Standard ${index + 1}`,
      sku:
        `SANITY-${createHash("sha1").update(`${product.handle || product._id}-${serialized}`).digest("hex").slice(0, 12)}`,
      options: selectedOptions,
      manage_inventory: false,
      prices: [{ amount: pickAmount(product.salePriceCents, product.priceCents, product.regularPriceCents), currency_code: "eur" }],
      weight: toFiniteNumber(product.weight),
      length: toFiniteNumber(product.length),
      width: toFiniteNumber(product.width),
      height: toFiniteNumber(product.height),
      thumbnail: getEffectiveMainImageUrl(product) || undefined,
    }
  })
}

function buildProductOptionsAndVariants(product: SanityProductSyncPayload) {
  const configured = buildConfiguredProductOptionsAndVariants(product)
  if (configured) {
    return configured
  }

  const sourceVariations = (product.variations || []).filter(Boolean)

  if (!sourceVariations.length) {
    return {
      options: [{ title: "Format", values: ["Standard"] }],
      variants: [
        {
          title: "Standard",
          sku: `SANITY-${createHash("sha1").update(product.handle || product._id).digest("hex").slice(0, 12)}`,
          options: { Format: "Standard" },
          manage_inventory: false,
          prices: [{ amount: pickAmount(product.salePriceCents, product.priceCents, product.regularPriceCents), currency_code: "eur" }],
          weight: toFiniteNumber(product.weight),
          length: toFiniteNumber(product.length),
          width: toFiniteNumber(product.width),
          height: toFiniteNumber(product.height),
          thumbnail: getEffectiveMainImageUrl(product) || undefined,
        },
      ],
      metadataAxes: [],
    }
  }

  const optionMap = new Map<string, Set<string>>()

  for (const variation of sourceVariations) {
    for (const attribute of variation.attributes || []) {
      const optionTitle = (attribute?.name || attribute?.slug || "Option").trim()
      const optionValue = (attribute?.option || "").trim()

      if (!optionTitle || !optionValue) continue

      const values = optionMap.get(optionTitle) || new Set<string>()
      values.add(optionValue)
      optionMap.set(optionTitle, values)
    }
  }

  const options =
    optionMap.size > 0
      ? Array.from(optionMap.entries()).map(([title, values]) => ({ title, values: Array.from(values) }))
      : [{ title: "Format", values: ["Standard"] }]

  const variants = sourceVariations.map((variation, index) => {
    const selectedOptions =
      optionMap.size > 0
        ? Object.fromEntries(
            Array.from(optionMap.keys()).map((title) => {
              const attr = (variation.attributes || []).find((item) => {
                return normalizeKey(item?.name || item?.slug || "") === normalizeKey(title)
              })

              return [title, (attr?.option || "Standard").trim() || "Standard"]
            }),
          )
        : { Format: "Standard" }

    return {
      title: Object.values(selectedOptions).join(" / ") || `Standard ${index + 1}`,
      sku:
        (variation.sku || "").trim() ||
        `SANITY-${createHash("sha1").update(`${product.handle || product._id}-${index}`).digest("hex").slice(0, 12)}`,
      options: selectedOptions,
      manage_inventory: false,
      prices: [
        {
          amount: pickAmount(variation.sale_price, variation.price, variation.regular_price, product.salePriceCents, product.priceCents),
          currency_code: "eur",
        },
      ],
      weight: toFiniteNumber(variation.weight) ?? toFiniteNumber(product.weight),
      length: toFiniteNumber(variation.length) ?? toFiniteNumber(product.length),
      width: toFiniteNumber(variation.width) ?? toFiniteNumber(product.width),
      height: toFiniteNumber(variation.height) ?? toFiniteNumber(product.height),
      thumbnail: variation.image?.assetUrl || variation.image?.url || variation.image?.src || getEffectiveMainImageUrl(product) || undefined,
    }
  })

  return { options, variants, metadataAxes: [] }
}

async function findExistingProductForSanityProduct(
  container: ContainerLike,
  sanityDocumentId: string,
  handle: string,
) {
  const query = container.resolve<any>(ContainerRegistrationKeys.QUERY)
  const all: ExistingProductRecord[] = []
  const take = 200
  let skip = 0

  while (true) {
    const { data } = await query.graph({
      entity: "product",
      fields: ["id", "handle", "metadata"],
      pagination: { take, skip },
    })

    const rows = (data || []) as ExistingProductRecord[]
    all.push(...rows)

    if (rows.length < take) {
      break
    }

    skip += take
  }

  const bySanityDocumentId = all.find((item) => item.metadata?.sanity_document_id === sanityDocumentId)
  if (bySanityDocumentId) {
    return bySanityDocumentId
  }

  return all.find((item) => item.handle === handle) || null
}

async function findExistingVariantsByProduct(container: ContainerLike, productId: string) {
  const query = container.resolve<any>(ContainerRegistrationKeys.QUERY)
  const { data } = await query.graph({
    entity: "product_variant",
    fields: ["id", "sku", "product.id"],
  })

  return ((data || []) as ExistingVariantRecord[]).filter((variant) => variant.product?.id === productId)
}

export async function fetchSanityProductForSync(documentId: string): Promise<SanityProductSyncPayload | null> {
  const sanityEnv = readSanityEnv()

  if (!sanityEnv.projectId) {
    throw new Error("Missing SANITY_PROJECT_ID for product sync.")
  }

  const query = `*[
    _type == "product" &&
    _id == $id
  ][0]{
    _id,
    title,
    status,
    "isActive": coalesce(isActive, true),
    "handle": slug.current,
    shortDescription,
    careText,
    "description": coalesce(pt::text(descriptionRich), description),
    "themeOptions": select(
      showThemes == true => coalesce(
        themes[defined(@->title) && coalesce(@->isActive, true) == true]->title,
        themeOptions,
        attributes[slug == "je-personnalise-mon-theme"][0].options
      ),
      []
    ),
    priceCents,
    regularPriceCents,
    salePriceCents,
    stockStatus,
    stockQuantity,
    weight,
    length,
    width,
    height,
    "mainImageAssetUrl": mainImage.asset->url,
    imageUrl,
    "gallery": gallery[]{
      _key,
      "assetUrl": image.asset->url,
      url
    }[defined(image.asset->url) || defined(url)],
    "personalizationFields": personalizationFields[]{
      label,
      "key": select(defined(key.current) => key.current, key),
      inputType,
      useThemeOptions,
      options,
      helperText,
      "helperImageUrl": helperImage.asset->url,
      helperImageLabel,
      showLabel,
      placeholder,
      defaultValue,
      maxLength,
      required
    },
    "variationAxes": variationAxes[]{
      label,
      key,
      kind,
      values
    },
    "variantDefinitions": variantDefinitions[]{
      isEnabled,
      title,
      sku,
      price,
      regular_price,
      sale_price,
      stock_status,
      stock_quantity,
      weight,
      length,
      width,
      height,
      image{
        "assetUrl": asset.asset->url,
        src,
        url
      },
      selections[]{
        axisKey,
        value
      }
    },
    variations[]{
      sku,
      price,
      regular_price,
      sale_price,
      stock_status,
      stock_quantity,
      weight,
      length,
      width,
      height,
      image{
        "assetUrl": asset.asset->url,
        src,
        url
      },
      attributes[]{
        name,
        slug,
        option
      }
    }
  }`

  const url = new URL(`https://${sanityEnv.projectId}.api.sanity.io/v${sanityEnv.apiVersion}/data/query/${sanityEnv.dataset}`)
  url.searchParams.set("query", query)
  url.searchParams.set("$id", JSON.stringify(documentId))

  const headers: Record<string, string> = {
    accept: "application/json",
  }

  if (sanityEnv.token) {
    headers.authorization = `Bearer ${sanityEnv.token}`
  }

  const response = await fetch(url, {
    method: "GET",
    headers,
    cache: "no-store",
  })

  if (!response.ok) {
    throw new Error(`Sanity fetch failed (${response.status}).`)
  }

  const json = (await response.json()) as { result?: SanityProductSyncPayload | null }
  return json.result || null
}

export async function syncSanityProductToMedusa(
  container: ContainerLike,
  product: SanityProductSyncPayload,
  syncOptions?: SyncSanityProductToMedusaOptions,
) {
  const productWithS3Images = syncOptions?.allowSanityWriteback === false ? product : await migrateSanityProductImagesToS3(product)
  const handle = (productWithS3Images.handle || "").trim()
  const title = (productWithS3Images.title || "").trim()

  if (!handle || !title) {
    throw new Error("Missing product handle or title.")
  }

  const shouldPublish = productWithS3Images.status === "publish" && productWithS3Images.isActive !== false
  const existingProduct = await findExistingProductForSanityProduct(container, productWithS3Images._id, handle)
  const fulfillmentModuleService = container.resolve<any>(Modules.FULFILLMENT)
  const salesChannelModuleService = container.resolve<any>(Modules.SALES_CHANNEL)

  const [shippingProfile] = await fulfillmentModuleService.listShippingProfiles({ type: "default" })
  if (!shippingProfile) {
    throw new Error("No default shipping profile found.")
  }

  let [defaultSalesChannel] = await salesChannelModuleService.listSalesChannels({ name: "Default Sales Channel" })
  if (!defaultSalesChannel) {
    const channels = await salesChannelModuleService.listSalesChannels({}, { take: 1 })
    defaultSalesChannel = channels[0]
  }

  if (!defaultSalesChannel) {
    throw new Error("No sales channel found.")
  }

  const { options, variants, metadataAxes } = buildProductOptionsAndVariants(productWithS3Images)
  const themeOptions = (productWithS3Images.themeOptions || []).map((value) => (value || "").trim()).filter(Boolean)
  const personalizationFields = buildPersonalizationFields(productWithS3Images)
  const basePayload = {
    title,
    handle,
    description:
      (productWithS3Images.description || productWithS3Images.shortDescription || productWithS3Images.careText || "").trim() ||
      "Produit personnalise Stylunique.",
    status: shouldPublish ? ProductStatus.PUBLISHED : ProductStatus.DRAFT,
    shipping_profile_id: shippingProfile.id,
    images: buildImageList(productWithS3Images),
    weight: toFiniteNumber(productWithS3Images.weight),
    length: typeof productWithS3Images.length === "number" ? productWithS3Images.length : undefined,
    width: typeof productWithS3Images.width === "number" ? productWithS3Images.width : undefined,
    height: typeof productWithS3Images.height === "number" ? productWithS3Images.height : undefined,
    metadata: {
      sanity_document_id: productWithS3Images._id,
      sanity_handle: handle,
      synced_from_sanity: true,
      theme_options: themeOptions,
      supports_custom_theme: themeOptions.length > 0,
      personalization_fields: personalizationFields,
      variant_axes: metadataAxes,
    },
    sales_channels: [{ id: defaultSalesChannel.id }],
  }

  if (!existingProduct) {
    if (!shouldPublish) {
      return { action: "skipped" as const, handle }
    }

    await createProductsWorkflow(container as any).run({
      input: {
        products: [
          {
            ...basePayload,
            options,
            variants,
          },
        ],
      },
    })

    return { action: "created" as const, handle }
  }

  await updateProductsWorkflow(container as any).run({
    input: {
      products: [
        {
          id: existingProduct.id,
          title: basePayload.title,
          handle: basePayload.handle,
          description: basePayload.description,
          status: basePayload.status,
          weight: basePayload.weight,
          length: basePayload.length,
          width: basePayload.width,
          height: basePayload.height,
          images: basePayload.images,
          metadata: basePayload.metadata,
          options,
        },
      ],
    },
  })

  const existingVariants = await findExistingVariantsByProduct(container, existingProduct.id)
  const variantIdBySku = new Map(
    existingVariants
      .filter((variant) => typeof variant.sku === "string" && variant.sku.length > 0)
      .map((variant) => [variant.sku as string, variant.id]),
  )

  const desiredSkus = new Set(variants.map((variant) => variant.sku).filter(Boolean))
  const variantCreates = variants
    .filter((variant) => variant.sku && !variantIdBySku.has(variant.sku))
    .map((variant) => ({
      ...variant,
      product_id: existingProduct.id,
    }))
  const variantUpdates = variants
    .filter((variant) => variant.sku && variantIdBySku.has(variant.sku))
    .map((variant) => ({
      id: variantIdBySku.get(variant.sku)!,
      title: variant.title,
      options: variant.options,
      manage_inventory: false,
      prices: variant.prices,
      weight: variant.weight,
      length: variant.length,
      width: variant.width,
      height: variant.height,
      thumbnail: variant.thumbnail,
    }))
  const variantDeletes = existingVariants
    .filter((variant) => {
      const sku = typeof variant.sku === "string" ? variant.sku.trim() : ""
      return !sku || !desiredSkus.has(sku)
    })
    .map((variant) => variant.id)

  if (variantCreates.length || variantUpdates.length || variantDeletes.length) {
    await batchProductVariantsWorkflow(container as any).run({
      input: {
        create: variantCreates,
        update: variantUpdates,
        delete: variantDeletes,
      },
    })
  }

  return { action: "updated" as const, handle }
}
