const t = require('@babel/types')

const {
  deriveArrayEntryFieldName,
  templatePlaceholder,
  literalFieldInitializer,
  loadFieldMap,
  saveFieldMap,
} = require('./template-field-tagger')
const {
  formatV2ConstJsdocBlock,
  formatGroupStartLineComments,
  formatGroupEndLineComments,
} = require('./field-hoist')

/** Module-scope config arrays: STEPS, ITEMS, … (limits false positives vs camelCase locals). */
const SCREAMING_SNAKE = /^[A-Z][A-Z0-9_]*$/

function inferKindFromKey(key) {
  const k = key.toLowerCase()
  if (
    /(^|_)(title|description|headline|subhead|content|copy|body|text|alt)$/i.test(key) ||
    k === 'label' ||
    k === 'subtitle'
  ) {
    return 'RichText'
  }
  // panelWidth, maxHeight, etc. (suffix match, not only exact "width")
  if (/(width|height)$/i.test(key)) return 'Number'

  const linkish =
    /^href$/i.test(key) ||
    /^to$/i.test(key) ||
    /^pathname$/i.test(key) ||
    /href$/i.test(key)
  const imageish =
    /^src$/i.test(key) ||
    /^poster$/i.test(key) ||
    /^thumbnail$/i.test(key) ||
    /^avatar$/i.test(key) ||
    /^image$/i.test(key) ||
    /^imageurl$/i.test(key) ||
    /^imagesrc$/i.test(key) ||
    /src$/i.test(key)

  if (linkish && !imageish) return 'Link'
  if (imageish) return 'Image'

  return 'String'
}

/**
 * Legacy `$(…)` fields for RichText / Image / Link resolve to objects at runtime; lifted
 * array entries must reference scalars (`.text`, `.url`, `.href`). V2 templates hoist plain
 * literals, so identifiers stay correct.
 * @param {string} groupingStyle
 * @returns {import('@babel/types').Expression}
 */
function arrayFieldReferenceExpression(fieldName, kind, groupingStyle) {
  if (groupingStyle !== 'lineComments') {
    return t.identifier(fieldName)
  }
  if (kind === 'RichText') {
    return t.memberExpression(t.identifier(fieldName), t.identifier('text'), false)
  }
  if (kind === 'Image') {
    return t.memberExpression(t.identifier(fieldName), t.identifier('url'), false)
  }
  if (kind === 'Link') {
    return t.memberExpression(t.identifier(fieldName), t.identifier('href'), false)
  }
  return t.identifier(fieldName)
}

function getObjectElementId(objExpr, index) {
  for (const prop of objExpr.properties || []) {
    if (!t.isObjectProperty(prop)) continue
    const key = t.isIdentifier(prop.key)
      ? prop.key.name
      : t.isStringLiteral(prop.key)
        ? prop.key.value
        : null
    if (key === 'id' && t.isStringLiteral(prop.value)) return prop.value.value
  }
  return `item${index}`
}

function unwrapArrayInit(init) {
  if (t.isTSAsExpression(init) && t.isArrayExpression(init.expression)) {
    return { arrayExpr: init.expression }
  }
  if (t.isArrayExpression(init)) {
    return { arrayExpr: init }
  }
  return null
}

function isExtractableLiteral(node) {
  return t.isStringLiteral(node) || t.isNumericLiteral(node) || t.isBooleanLiteral(node)
}

function defaultValueFromLiteral(node) {
  if (t.isStringLiteral(node)) return node.value
  if (t.isNumericLiteral(node)) return node.value
  if (t.isBooleanLiteral(node)) return node.value
  return undefined
}

/** Top-level array elements that are plain literals (not `{ key: … }` rows). */
function inferKindFromScalarLiteral(node) {
  if (t.isStringLiteral(node)) return 'RichText'
  if (t.isNumericLiteral(node)) return 'Number'
  if (t.isBooleanLiteral(node)) return 'Boolean'
  return null
}

/** Stable `deriveArrayEntryFieldName` propertyKey + readable ordinal suffix for scalars. */
function scalarSyntheticPropertyKey(kind) {
  if (kind === 'RichText') return 'text'
  if (kind === 'Number') return 'value'
  if (kind === 'Boolean') return 'enabled'
  return 'value'
}

function attachGroupCommentsToDecls(declsForGroup, groupOrdinal, groupLabel, groupingStyle) {
  const n = declsForGroup.length
  declsForGroup.forEach(({ decl, kind }, fi) => {
    const isFirst = fi === 0
    const isLast = fi === n - 1
    if (groupingStyle === 'lineComments') {
      if (isFirst) {
        decl.leadingComments = [
          { type: 'CommentLine', value: formatGroupStartLineComments(groupOrdinal, groupLabel) },
        ]
      }
      if (isLast) {
        decl.trailingComments = [
          ...(decl.trailingComments || []),
          { type: 'CommentLine', value: formatGroupEndLineComments() },
        ]
      }
    } else if (groupingStyle === 'jsdoc') {
      decl.leadingComments = [{ type: 'CommentBlock', value: formatV2ConstJsdocBlock(groupLabel, kind) }]
    }
  })
}

/**
 * Lifts string/number/boolean literals from top-level `const NAME = [ { … } ]` (optional `as const`)
 * and from `const NAME = [ 'a', 'b', … ]` scalar rows, into template consts with grouping, and
 * rewrites the array to reference those identifiers.
 * Preserves non-literal values (e.g. `Icon: SomeComponent`).
 */
function extractModuleArrayConfig(ast, traverse, options) {
  const { fieldMapPath, groupingStyle = 'lineComments' } = options

  const report = { moduleArrayFieldCount: 0 }

  if (!fieldMapPath) {
    return { report }
  }

  const fieldMap = loadFieldMap(fieldMapPath)
  fieldMap.fields = fieldMap.fields || {}

  const jobs = []

  traverse(ast, {
    VariableDeclaration(p) {
      if (!p.parentPath.isProgram()) return
      if (p.node.kind !== 'const') return
      if (p.node.declarations.length !== 1) return

      const declPath = p.get('declarations.0')
      if (!declPath.isVariableDeclarator()) return
      if (!t.isIdentifier(declPath.node.id)) return

      const arrName = declPath.node.id.name
      if (!SCREAMING_SNAKE.test(arrName)) return

      const unwrapped = unwrapArrayInit(declPath.node.init)
      if (!unwrapped) return
      const { arrayExpr } = unwrapped
      if (!arrayExpr.elements.length) return

      const newElements = []
      const allDecls = []
      let groupOrdinal = 0

      for (let ei = 0; ei < arrayExpr.elements.length; ei += 1) {
        const el = arrayExpr.elements[ei]
        if (t.isObjectExpression(el)) {
          const elementId = getObjectElementId(el, ei)
          const groupLabel = `${arrName} (${elementId})`

          const newProps = []
          const declsForGroup = []

          for (const prop of el.properties) {
            if (!t.isObjectProperty(prop)) {
              newProps.push(prop)
              continue
            }

            const keyName = t.isIdentifier(prop.key)
              ? prop.key.name
              : t.isStringLiteral(prop.key)
                ? prop.key.value
                : null
            if (keyName == null) {
              newProps.push(prop)
              continue
            }

            const val = prop.value
            if (!isExtractableLiteral(val)) {
              newProps.push(prop)
              continue
            }

            const kind = inferKindFromKey(keyName)
            const defaultValue = defaultValueFromLiteral(val)
            const fieldName = deriveArrayEntryFieldName({
              arrayName: arrName,
              elementIndex: ei,
              propertyKey: keyName,
              kind,
              fieldMap,
            })

            const init =
              groupingStyle === 'jsdoc'
                ? literalFieldInitializer(fieldName, kind, defaultValue)
                : templatePlaceholder(fieldName, kind, defaultValue)
            const decl = t.variableDeclaration('const', [
              t.variableDeclarator(t.identifier(fieldName), init),
            ])

            declsForGroup.push({ decl, kind })
            report.moduleArrayFieldCount += 1

            newProps.push(
              t.objectProperty(
                prop.key,
                arrayFieldReferenceExpression(fieldName, kind, groupingStyle),
                prop.computed,
              ),
            )
          }

          if (declsForGroup.length) {
            groupOrdinal += 1
            attachGroupCommentsToDecls(declsForGroup, groupOrdinal, groupLabel, groupingStyle)
            declsForGroup.forEach(({ decl }) => {
              allDecls.push(decl)
            })
          }

          newElements.push(t.objectExpression(newProps))
        } else if (isExtractableLiteral(el)) {
          const kind = inferKindFromScalarLiteral(el)
          const defaultValue = defaultValueFromLiteral(el)
          const pk = scalarSyntheticPropertyKey(kind)
          const fieldName = deriveArrayEntryFieldName({
            arrayName: arrName,
            elementIndex: ei,
            propertyKey: pk,
            kind,
            fieldMap,
          })

          const init =
            groupingStyle === 'jsdoc'
              ? literalFieldInitializer(fieldName, kind, defaultValue)
              : templatePlaceholder(fieldName, kind, defaultValue)
          const decl = t.variableDeclaration('const', [
            t.variableDeclarator(t.identifier(fieldName), init),
          ])

          groupOrdinal += 1
          const groupLabel = `${arrName} (item${ei})`
          attachGroupCommentsToDecls([{ decl, kind }], groupOrdinal, groupLabel, groupingStyle)
          allDecls.push(decl)
          report.moduleArrayFieldCount += 1

          newElements.push(arrayFieldReferenceExpression(fieldName, kind, groupingStyle))
        } else {
          newElements.push(el)
        }
      }

      if (!allDecls.length) return

      const newArray = t.arrayExpression(newElements)
      // Drop `as const` / TS assertions so output is valid plain JSX for CMS pipelines.
      declPath.node.init = newArray

      jobs.push({
        path: p,
        decls: allDecls,
        start: typeof p.node.start === 'number' ? p.node.start : 0,
      })
    },
  })

  jobs.sort((a, b) => b.start - a.start)
  for (const job of jobs) {
    job.path.insertBefore(job.decls)
  }

  saveFieldMap(fieldMapPath, fieldMap)
  return { report }
}

module.exports = {
  extractModuleArrayConfig,
}
