import { readFile } from "node:fs/promises"
import path from "node:path"
import { ExecArgs } from "@medusajs/framework/types"
import { ContainerRegistrationKeys, Modules, ProductStatus } from "@medusajs/framework/utils"
import {
  createInventoryLevelsWorkflow,
  createProductsWorkflow,
  deleteProductsWorkflow,
  linkSalesChannelsToStockLocationWorkflow,
  updateProductVariantsWorkflow,
  updateProductsWorkflow,
} from "@medusajs/medusa/core-flows"

type WooExport = {
  sourceApi?: string
  products?: Array<{
    id: number
    name: string
    slug: string
    status?: string
    description?: string
    short_description?: string
    price?: string | number | null
    regular_price?: string | number | null
    sale_price?: string | number | null
    images?: Array<{ src?: string }>
    weight?: string | number | null
    dimensions?: {
      length?: string | number | null
      width?: string | number | null
      height?: string | number | null
    } | null
    attributes?: Array<{
      name?: string
      has_variations?: boolean
      variation?: boolean
      options?: string[]
      terms?: Array<{ name?: string; slug?: string }>
    }>
    variations?: Array<{
      id?: number
      weight?: string | number | null
      dimensions?: {
        length?: string | number | null
        width?: string | number | null
        height?: string | number | null
      } | null
      price?: string | number | null
      regular_price?: string | number | null
      sale_price?: string | number | null
      attributes?: Array<{ name?: string; slug?: string; value?: string | null; option?: string | null }>
      image?:
        | string
        | {
            src?: string
            url?: string
          }
    }>
  }>
}

type StoreRecord = {
  id: string
  default_location_id?: string | null
}

type StockLocationRecord = {
  id: string
}

type InventoryItemRecord = {
  id: string
}

type InventoryLevelRecord = {
  inventory_item_id: string
}

type ProductVariantRecord = {
  id: string
  sku?: string | null
}

const THEME_OPTIONS = [
  "Velo jaune",
  "Peintre rouge",
  "Jungle - leopard et zebre",
  "Petit dinosaure vert",
  "Danseuse ballerine rose",
  "Fleur rouge orange",
  "Chic - or paillete et bordeaux",
  "Girly soft rose pale",
  "Lettre postale",
  "Ferme - rouge et blanc",
  "Paquerette orangee",
  "Licorne rose pailletee",
  "Poisson bulles orange",
  "Paquerette violet",
  "Welcome Baby - ourson marron",
  "Vert sauge or",
  "Renard violet pastel",
  "Moto cross - noir et orange",
  "Camion sur la route",
  "Retro multicolore",
  "Dia de los muertos - garcon",
  "Dia de los muertos - fille",
  "Veux tu etre mon parrain / ma marraine ?",
  "Retro - vert et creme",
]

function toAmount(value: unknown, sourceApi: string): number {
  const isStoreApi = sourceApi === "wc/store/v1"
  const parse = (() => {
    if (typeof value === "number" && Number.isFinite(value)) {
      return { n: value, isIntegerText: Number.isInteger(value) }
    }
    if (typeof value === "string") {
      const text = value.trim()
      if (!text) return null
      const isIntegerText = /^\d+$/.test(text)
      const n = Number(text.replace(",", "."))
      if (Number.isFinite(n)) {
        return { n, isIntegerText }
      }
    }
    return null
  })()

  if (!parse) return 0

  const { n, isIntegerText } = parse
  if (!isStoreApi) {
    return Math.max(0, Math.round(n * 100))
  }

  // wc/store/v1 often returns prices in minor units; if value looks like "900",
  // keep as-is, otherwise treat "9" as 9 EUR and convert to cents.
  if (isIntegerText && n >= 100) {
    return Math.max(0, Math.round(n))
  }

  return Math.max(0, Math.round(n * 100))
}

function pickPrice(...values: Array<string | number | null | undefined>) {
  for (const value of values) {
    if (value === null || value === undefined) continue
    if (typeof value === "string" && value.trim() === "") continue
    return value
  }
  return null
}

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

function normalizeText(value: string): string {
  return value
    .normalize("NFD")
    .replace(/[\u0300-\u036f]/g, "")
    .toLowerCase()
}

function isToniesProduct(product: NonNullable<WooExport["products"]>[number]): boolean {
  const searchable = normalizeText(
    [
      product.name || "",
      product.slug || "",
      ...((product as any).tags || []).map((tag: { name?: string }) => tag?.name || ""),
      ...((product as any).categories || []).map((category: { name?: string }) => category?.name || ""),
    ].join(" ")
  )

  return searchable.includes("tonie") || searchable.includes("tonies") || searchable.includes("toniebox")
}

function unique<T>(items: T[]): T[] {
  return [...new Set(items)]
}

function chunk<T>(items: T[], size: number): T[][] {
  const out: T[][] = []
  for (let i = 0; i < items.length; i += size) {
    out.push(items.slice(i, i + size))
  }
  return out
}

function extractVariationImageUrl(variation: { image?: string | { src?: string; url?: string } } | undefined) {
  if (!variation?.image) return null
  if (typeof variation.image === "string") return variation.image
  return variation.image.src || variation.image.url || null
}

function toNumberOrUndefined(value: unknown): number | undefined {
  if (value === null || value === undefined) {
    return undefined
  }

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

  if (typeof value === "string") {
    const normalized = value.trim().replace(",", ".")
    if (!normalized) {
      return undefined
    }
    const parsed = Number(normalized)
    return Number.isFinite(parsed) ? parsed : undefined
  }

  return undefined
}

function toPhysicalAttributes(input: {
  weight?: unknown
  dimensions?: { length?: unknown; width?: unknown; height?: unknown } | null
}) {
  return {
    weight: toNumberOrUndefined(input.weight),
    length: toNumberOrUndefined(input.dimensions?.length),
    width: toNumberOrUndefined(input.dimensions?.width),
    height: toNumberOrUndefined(input.dimensions?.height),
  }
}

function buildVariants(product: NonNullable<WooExport["products"]>[number], sourceApi: string) {
  const fallbackAmount = toAmount(
    pickPrice(product.sale_price, product.price, product.regular_price),
    sourceApi
  )
  const fallbackImage = product.images?.[0]?.src
  const attrs = (product.attributes || []).filter(
    (attr) =>
      (attr?.has_variations || attr?.variation) &&
      (((attr.terms || []).length > 0) || ((attr.options || []).length > 0))
  )

  if (!attrs.length) {
    const physical = toPhysicalAttributes({
      weight: product.weight,
      dimensions: product.dimensions,
    })

    return {
      options: [{ title: "Format", values: ["Standard"] }],
      variants: [
        {
          title: "Standard",
          sku: `WOO-${product.id}`,
          options: { Format: "Standard" },
          ...physical,
          prices: [{ amount: fallbackAmount, currency_code: "eur" }],
          thumbnail: fallbackImage || undefined,
        },
      ],
    }
  }

  const optionModels = attrs.map((attr) => {
    const title = (attr.name || "Option").trim()
    const slugToName: Record<string, string> = {}
    const termValues = (attr.terms || [])
      .map((term) => {
        const name = (term?.name || "").trim()
        const slug = (term?.slug || "").trim()
        if (name) {
          slugToName[normalizeKey(name)] = name
        }
        if (slug && name) {
          slugToName[normalizeKey(slug)] = name
        }
        return name
      })
      .filter(Boolean)
    const optionValues = (attr.options || []).map((o) => (o || "").trim()).filter(Boolean)
    const values = unique([...termValues, ...optionValues])
    for (const value of values) {
      slugToName[normalizeKey(value)] = value
    }
    return { title, values, slugToName }
  })
  const options = optionModels.map(({ title, values }) => ({ title, values }))

  const variations = (product.variations || []).filter((variation) => (variation.attributes || []).length)

  if (!variations.length) {
    const physical = toPhysicalAttributes({
      weight: product.weight,
      dimensions: product.dimensions,
    })

    const selected = Object.fromEntries(options.map((o) => [o.title, o.values[0] || "Standard"]))
    return {
      options,
      variants: [
        {
          title: options.map((o) => selected[o.title]).join(" / "),
          sku: `WOO-${product.id}`,
          options: selected,
          ...physical,
          prices: [{ amount: fallbackAmount, currency_code: "eur" }],
        },
      ],
    }
  }

  const variants: Array<{
    title: string
    sku: string
    options: Record<string, string>
    prices: Array<{ amount: number; currency_code: string }>
    thumbnail?: string
    weight?: number
    length?: number
    width?: number
    height?: number
  }> = []
  const seen = new Set<string>()

  for (const [index, variation] of variations.entries()) {
    const selected: Record<string, string> = {}

    for (const optionModel of optionModels) {
      const optionKey = normalizeKey(optionModel.title)
      const match = (variation.attributes || []).find(
        (entry) =>
          normalizeKey(entry?.name || "") === optionKey ||
          normalizeKey(entry?.slug || "") === normalizeKey(`pa_${optionKey}`)
      )

      let value = ((match?.value || match?.option) || "").trim()
      if (!value) {
        value = optionModel.values[0] || "Standard"
      } else {
        const normalized = normalizeKey(value)
        value =
          optionModel.slugToName[normalized] ||
          optionModel.values.find((candidate) => normalizeKey(candidate) === normalized) ||
          value
      }

      if (!optionModel.values.includes(value)) {
        optionModel.values.push(value)
      }
      selected[optionModel.title] = value
    }

    const signature = JSON.stringify(selected)
    if (seen.has(signature)) {
      continue
    }

    seen.add(signature)
    variants.push({
      title: options.map((o) => selected[o.title]).join(" / "),
      sku: `WOO-${product.id}-${variation.id || index + 1}`,
      options: selected,
      ...toPhysicalAttributes({
        weight: variation.weight,
        dimensions: variation.dimensions,
      }),
      prices: [
        {
          amount:
            toAmount(
              pickPrice(variation.sale_price, variation.price, variation.regular_price),
              sourceApi
            ) || fallbackAmount,
          currency_code: "eur",
        },
      ],
      thumbnail: extractVariationImageUrl(variation) || fallbackImage || undefined,
    })
  }

  if (!variants.length) {
    const physical = toPhysicalAttributes({
      weight: product.weight,
      dimensions: product.dimensions,
    })

    const selected = Object.fromEntries(options.map((o) => [o.title, o.values[0] || "Standard"]))
    variants.push({
      title: options.map((o) => selected[o.title]).join(" / "),
      sku: `WOO-${product.id}`,
      options: selected,
      ...physical,
      prices: [{ amount: fallbackAmount, currency_code: "eur" }],
      thumbnail: fallbackImage || undefined,
    })
  }

  return { options, variants }
}

export default async function importWooToMedusa({ container }: ExecArgs) {
  const logger = container.resolve(ContainerRegistrationKeys.LOGGER)
  const query = container.resolve(ContainerRegistrationKeys.QUERY)
  const fulfillmentModuleService = container.resolve(Modules.FULFILLMENT)
  const salesChannelModuleService = container.resolve(Modules.SALES_CHANNEL)

  const inputPath =
    process.env.WOO_OUTPUT_PATH ||
    path.resolve(process.cwd(), "../../data/woo-export.json")

  logger.info(`Loading Woo export from ${inputPath}`)
  const raw = await readFile(inputPath, "utf8")
  const data = JSON.parse(raw) as WooExport
  const sourceApi = data.sourceApi || "wc/v3"
  const wooProducts = (data.products || []).filter((p) => p?.slug)

  if (!wooProducts.length) {
    logger.info("No products found in Woo export file.")
    return
  }

  const [shippingProfile] = await fulfillmentModuleService.listShippingProfiles({
    type: "default",
  })

  if (!shippingProfile) {
    throw new Error("No default shipping profile found. Run seed first.")
  }

  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. Run seed first.")
  }

  const { data: stores } = await query.graph({
    entity: "store",
    fields: ["id", "default_location_id"],
  })

  const store = (stores?.[0] || null) as StoreRecord | null
  let stockLocationId = store?.default_location_id || null

  if (!stockLocationId) {
    const { data: stockLocations } = await query.graph({
      entity: "stock_location",
      fields: ["id"],
      pagination: { take: 1 },
    })

    stockLocationId = (stockLocations?.[0] as StockLocationRecord | undefined)?.id || null
  }

  if (!stockLocationId) {
    throw new Error("No stock location found. Run seed first.")
  }

  await linkSalesChannelsToStockLocationWorkflow(container).run({
    input: {
      id: stockLocationId,
      add: [defaultSalesChannel.id],
    },
  })

  const { data: existing } = await query.graph({
    entity: "product",
    fields: ["id", "handle"],
  })

  const wooHandles = new Set(wooProducts.map((p) => p.slug))
  const idsToDelete = (existing || [])
    .filter((p: { id: string; handle: string }) => wooHandles.has(p.handle))
    .map((p: { id: string }) => p.id)

  let recreateExistingProducts = true
  if (idsToDelete.length) {
    logger.info(`Deleting ${idsToDelete.length} existing Woo products in Medusa...`)
    try {
      await deleteProductsWorkflow(container).run({
        input: { ids: idsToDelete },
      })
    } catch (error) {
      recreateExistingProducts = false
      logger.warn(
        `Delete skipped (existing reservations). Switching to update mode for existing products: ${
          error instanceof Error ? error.message : "unknown error"
        }`
      )
    }
  }

  const payload: any[] = wooProducts.map((product) => {
      const imageUrl = product.images?.[0]?.src
      const description = (product.description || product.short_description || "").trim()
      const { options, variants } = buildVariants(product, sourceApi)
      const productPhysicalAttributes = toPhysicalAttributes({
        weight: product.weight,
        dimensions: product.dimensions,
      })
      const supportsThemes = !isToniesProduct(product)

      return {
        title: product.name || `Produit ${product.id}`,
        handle: product.slug,
        description: description || "Produit personnalise Stylunique.",
        status: ProductStatus.PUBLISHED,
        shipping_profile_id: shippingProfile.id,
        images: imageUrl ? [{ url: imageUrl }] : [],
        ...productPhysicalAttributes,
        options,
        variants,
        metadata: {
          theme_attribute_name: "Je personnalise mon theme",
          theme_attribute_slug: "je-personnalise-mon-theme",
          theme_options: supportsThemes ? THEME_OPTIONS : [],
          supports_custom_theme: supportsThemes,
        },
        sales_channels: [{ id: defaultSalesChannel.id }],
      }
    })

  if (!payload.length) {
    logger.info("No Woo products to import.")
    return
  }

  if (recreateExistingProducts) {
    logger.info(`Creating ${payload.length} Woo products with variants in Medusa...`)
    for (const [index, group] of chunk(payload, 10).entries()) {
      logger.info(`Creating batch ${index + 1}/${Math.ceil(payload.length / 10)} (${group.length} products)...`)
      await createProductsWorkflow(container).run({
        input: {
          products: group,
        },
      })
    }
  } else {
    const existingByHandle = new Map(
      ((existing || []) as Array<{ id: string; handle: string }>).map((entry) => [entry.handle, entry.id])
    )

    const toUpdate = payload
      .filter((product) => existingByHandle.has(product.handle))
      .map((product) => ({
        id: existingByHandle.get(product.handle)!,
        title: product.title,
        description: product.description,
        weight: product.weight,
        length: product.length,
        width: product.width,
        height: product.height,
        metadata: product.metadata,
      }))

    const toCreate = payload.filter((product) => !existingByHandle.has(product.handle))

    if (toUpdate.length) {
      logger.info(`Updating ${toUpdate.length} existing Woo products (including physical attributes)...`)
      for (const [index, group] of chunk(toUpdate, 20).entries()) {
        logger.info(`Updating batch ${index + 1}/${Math.ceil(toUpdate.length / 20)} (${group.length} products)...`)
        await updateProductsWorkflow(container).run({
          input: {
            products: group,
          },
        })
      }

      const { data: existingVariants } = await query.graph({
        entity: "product_variant",
        fields: ["id", "sku"],
      })

      const variantsBySku = new Map(
        ((existingVariants || []) as ProductVariantRecord[])
          .filter((variant) => typeof variant.sku === "string" && variant.sku.length > 0)
          .map((variant) => [variant.sku as string, variant.id])
      )

      const variantPriceUpdates = payload
        .filter((product) => existingByHandle.has(product.handle))
        .flatMap((product) =>
          (product.variants || [])
            .filter((variant: { sku?: string; prices?: Array<{ amount: number; currency_code: string }> }) =>
              Boolean(variant?.sku && variantsBySku.has(variant.sku) && (variant.prices || []).length)
            )
            .map((variant: { sku: string; prices: Array<{ amount: number; currency_code: string }> }) => ({
              id: variantsBySku.get(variant.sku)!,
              prices: variant.prices,
            }))
        )

      if (variantPriceUpdates.length) {
        logger.info(`Updating prices for ${variantPriceUpdates.length} existing variants...`)
        for (const [index, group] of chunk(variantPriceUpdates, 50).entries()) {
          logger.info(
            `Updating variant price batch ${index + 1}/${Math.ceil(variantPriceUpdates.length / 50)} (${group.length} variants)...`
          )
          await updateProductVariantsWorkflow(container).run({
            input: {
              product_variants: group,
            },
          })
        }
      }
    }

    if (toCreate.length) {
      logger.info(`Creating ${toCreate.length} new Woo products in Medusa...`)
      for (const [index, group] of chunk(toCreate, 10).entries()) {
        logger.info(`Creating batch ${index + 1}/${Math.ceil(toCreate.length / 10)} (${group.length} products)...`)
        await createProductsWorkflow(container).run({
          input: {
            products: group,
          },
        })
      }
    }
  }

  const { data: inventoryItems } = await query.graph({
    entity: "inventory_item",
    fields: ["id"],
  })

  const { data: levelsAtLocation } = await query.graph({
    entity: "inventory_level",
    fields: ["inventory_item_id"],
    filters: {
      location_id: stockLocationId,
    },
  })

  const existingItemIds = new Set(
    ((levelsAtLocation || []) as InventoryLevelRecord[]).map((level) => level.inventory_item_id)
  )

  const missingLevels = ((inventoryItems || []) as InventoryItemRecord[])
    .filter((item) => !existingItemIds.has(item.id))
    .map((item) => ({
      location_id: stockLocationId as string,
      inventory_item_id: item.id,
      stocked_quantity: 1000000,
    }))

  if (missingLevels.length) {
    await createInventoryLevelsWorkflow(container).run({
      input: {
        inventory_levels: missingLevels,
      },
    })
    logger.info(`Created ${missingLevels.length} inventory levels for imported variants.`)
  }

  logger.info("Woo products imported to Medusa with variants.")
}
