import type { PartMaterialEntry, PartMaterialMap } from '../../api/cad'
import type { MaterialPBR, MaterialPBRMap } from '../../api/assetLibraries'

/**
 * Normalize a GLB mesh name by stripping suffixes added by the export pipeline:
 * - OCC RWGltf_CafWriter adds "_AF0", "_AF1", … for repeated assembly instances
 * - Blender adds ".001", ".002", … for name deduplication on re-import
 *
 * Mirrors the logic in render-worker/scripts/export_gltf.py (lines 107-114).
 *
 * Examples:
 *   "Ring_AF3"                       → "Ring"
 *   "Ring_AF0_AF1"                   → "Ring"   (nested suffixes — loop until stable)
 *   "Cage.001"                       → "Cage"
 *   "Cage.001_AF2"                   → "Cage"
 *   "KOMP_ASM_1_AF0_ASM"             → "KOMP_ASM_1"  (_AF0_ASM variant)
 *   "GE360-HF_000_P_ASM_1_AF0_ASM"  → "GE360-HF_000_P_ASM_1"
 *   "PlainPart"                      → "PlainPart"
 */
export function normalizeMeshName(name: string): string {
  // Strip Blender dedup suffix (.001, .002, …)
  let n = name.replace(/\.\d{3}$/, '')
  // Strip OCC assembly-instance suffix — handles _AF0, _AF1, _AF0_ASM, _AF1_ASM patterns
  // The optional (_ASM)? group catches assembly-node variants like _AF0_ASM
  let prev = ''
  while (prev !== n) { prev = n; n = n.replace(/_AF\d+(_ASM)?$/i, '') }
  return n
}

// ---------------------------------------------------------------------------
// resolvePartMaterial
// ---------------------------------------------------------------------------

/**
 * Resolve a material entry for a (already-normalized) GLB mesh name.
 *
 * OCC's GLB exporter strips certain path suffixes (_ASM_1, _1, _AF\d+_\d+)
 * that cadquery keeps when parsing the STEP topology. This means stored keys
 * from Excel-imported cad_part_materials may have extra suffixes compared to
 * the actual GLB mesh names.
 *
 * Strategy:
 *  1. Exact match: partMaterials[meshKey]
 *  2. Prefix match: find shortest stored key that starts with meshKey + '_'
 *     e.g. GLB "GE360-EIN_HAELFTE" matches stored "GE360-EIN_HAELFTE_AF0_1"
 *
 * Returns undefined when no match exists.
 */
export function resolvePartMaterial(
  meshKey: string,
  partMaterials: PartMaterialMap,
): PartMaterialEntry | undefined {
  // 1. Exact match
  if (partMaterials[meshKey]) return partMaterials[meshKey]
  // 2. Shortest stored key that starts with meshKey + '_'
  let bestKey: string | undefined
  for (const key of Object.keys(partMaterials)) {
    if (key.startsWith(meshKey + '_')) {
      if (!bestKey || key.length < bestKey.length) bestKey = key
    }
  }
  return bestKey ? partMaterials[bestKey] : undefined
}

// ---------------------------------------------------------------------------
// remapToPartKeys
// ---------------------------------------------------------------------------

/**
 * Remap a PartMaterialMap keyed by normalized OCC names to partKey slugs.
 *
 * When partKeyMap is available (from GLB extras), Excel-imported material keys
 * like "GE360-HF_000_P_ASM_1" are converted to partKey slugs like
 * "ge360_hf_000_p_asm_1" so they match what the viewer resolves each mesh to.
 *
 * Keys not found in partKeyMap are preserved as-is (backwards compat for old GLBs).
 */
export function remapToPartKeys(
  materials: PartMaterialMap,
  partKeyMap: Record<string, string>,
): PartMaterialMap {
  if (!partKeyMap || Object.keys(partKeyMap).length === 0) return materials
  const mapKeys = Object.keys(partKeyMap)
  const result: PartMaterialMap = {}
  for (const [key, entry] of Object.entries(materials)) {
    // 1. Exact match
    if (partKeyMap[key]) { result[partKeyMap[key]] = entry; continue }
    // 2. Prefix match: cad_part_materials may have extra _1 instance suffixes
    //    that partKeyMap doesn't (e.g. "PART_04_1" vs partKeyMap "PART_04")
    let matched = false
    for (const mk of mapKeys) {
      if (key.startsWith(mk + '_') || key === mk) {
        result[partKeyMap[mk]] = entry
        matched = true
        break
      }
    }
    if (!matched) result[key] = entry // preserve unmapped
  }
  return result
}

// ---------------------------------------------------------------------------
// convertCadPartMaterials
// ---------------------------------------------------------------------------

/**
 * Convert Product.cad_part_materials (list of {part_name, material}) to
 * the PartMaterialMap format used by the 3D viewers.
 *
 * - Skips entries with blank part_name or material
 * - Detects hex colors (starting with "#") vs library material names
 * - Normalizes part names with normalizeMeshName() so they match GLB mesh keys
 */
export function convertCadPartMaterials(
  items: Array<{ part_name: string; material: string }>,
): PartMaterialMap {
  const result: PartMaterialMap = {}
  for (const item of items) {
    if (!item.part_name.trim() || !item.material.trim()) continue
    const key = normalizeMeshName(item.part_name.trim())
    const value = item.material.trim()
    result[key] = { type: value.startsWith('#') ? 'hex' : 'library', value }
  }
  return result
}

// ---------------------------------------------------------------------------
// PBR material helpers
// ---------------------------------------------------------------------------

/**
 * Apply PBR material properties from the Blender asset library to a
 * Three.js MeshStandardMaterial.
 *
 * The `mat` parameter is typed as `any` to avoid importing THREE in this
 * utility module — callers pass `THREE.MeshStandardMaterial` instances.
 */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function applyPBRToMaterial(mat: any, pbr: MaterialPBR): void {
  if (!mat || !('color' in mat)) return
  // Use hex string via color.set() — reliable across all Three.js versions and
  // avoids colorSpace/gamma issues with setRGB() + ColorManagement.
  mat.color.set(pbrColorHex(pbr))
  mat.metalness = pbr.metallic
  mat.roughness = pbr.roughness
  if (pbr.transmission && pbr.transmission > 0.1) {
    mat.transparent = true
    mat.opacity = 1 - pbr.transmission * 0.7
  }
  mat.needsUpdate = true
}

/** Convert PBR base_color to hex string for UI swatches. */
export function pbrColorHex(pbr: MaterialPBR): string {
  const [r, g, b] = pbr.base_color
  return '#' + [r, g, b].map(v => Math.round(v * 255).toString(16).padStart(2, '0')).join('')
}

/**
 * Get a preview hex color for a material entry, using PBR data when available.
 * Replaces the old hardcoded SCHAEFFLER_COLORS lookup.
 */
export function previewColorForEntry(
  entry: PartMaterialEntry,
  pbrMap?: MaterialPBRMap,
): string {
  if (entry.type === 'hex') return entry.value
  if (pbrMap) {
    const pbr = pbrMap[entry.value]
    if (pbr) return pbrColorHex(pbr)
  }
  return '#888888'
}

// ---------------------------------------------------------------------------
// MeshRegistry — O(1) access to meshes by partKey, replaces O(n) traversals
// ---------------------------------------------------------------------------

/** A single entry in the mesh registry, linking a Three.js mesh to its partKey. */
export interface MeshRegistryEntry {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  mesh: any  // THREE.Mesh — typed as any to avoid importing THREE
  partKey: string
}

/**
 * Iterate all materials on a mesh, calling `fn` for each MeshStandardMaterial.
 * Handles both single and array materials safely.
 */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function forEachMeshMaterial(mesh: any, fn: (mat: any) => void): void {
  if (!mesh?.material) return
  const mats = Array.isArray(mesh.material) ? mesh.material : [mesh.material]
  for (const m of mats) {
    if (m && 'color' in m) fn(m)
  }
}

// ---------------------------------------------------------------------------
// Geometry merge helpers — for draw call batching in Performance mode
// ---------------------------------------------------------------------------

/**
 * Group registry entries by their resolved material key.
 *
 * Returns a Map where each key is a material identifier (library name, hex
 * color, or '__unassigned__') and each value is the list of registry entries
 * sharing that material. Groups with only 1 mesh are not worth merging and
 * are excluded.
 */
export function groupRegistryByMaterial(
  registry: MeshRegistryEntry[],
  partMaterials: PartMaterialMap,
): Map<string, MeshRegistryEntry[]> {
  const groups = new Map<string, MeshRegistryEntry[]>()
  for (const entry of registry) {
    const mat = resolvePartMaterial(entry.partKey, partMaterials)
    const key = mat ? `${mat.type}:${mat.value}` : '__unassigned__'
    const arr = groups.get(key)
    if (arr) arr.push(entry)
    else groups.set(key, [entry])
  }
  // Only keep groups with 2+ meshes (merging 1 mesh is pointless)
  for (const [key, arr] of groups) {
    if (arr.length < 2) groups.delete(key)
  }
  return groups
}
