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

const SUPPORTED_FIELD_TYPES = new Set([
  'RichText',
  'Color',
  'String',
  'Boolean',
  'Number',
  'Image',
  'Link',
  'BannerId',
  'Component',
])

/** Types edited in the visual/WYSIWYG surface; omit switches and abstract refs. */
const VISUAL_USAGE_TAG_FIELD_TYPES = new Set(['RichText', 'String', 'Image', 'Link', 'Color'])

function fieldTypeGetsVisualUsageTags(fieldType) {
  return fieldType && VISUAL_USAGE_TAG_FIELD_TYPES.has(fieldType)
}

function parseFieldTypeFromJsdocCommentValue(value) {
  if (!value || typeof value !== 'string') return null
  const m = value.match(/@type\s+([A-Za-z_][A-Za-z0-9_]*)\b/)
  if (!m) return null
  const typeName = m[1]
  if (!SUPPORTED_FIELD_TYPES.has(typeName)) return null
  return typeName
}

function fieldTypeFromLeadingComments(node) {
  const comments = (node && node.leadingComments) || []
  for (const c of comments) {
    if (!c || typeof c.value !== 'string') continue
    const typeName = parseFieldTypeFromJsdocCommentValue(c.value)
    if (typeName) return typeName
  }
  return null
}

/**
 * Legacy input (`template legacy from-jsx/from-tsx`) often contains:
 *   const x = $(x:RichText=...);
 * but `$()` contents are not always parseable by Babel due to `:` / `[]` tokens.
 * So we extract legacy field types from raw source via regex.
 */
function extractLegacyFieldTypesFromSource(source) {
  const out = new Map()
  if (!source || typeof source !== 'string') return out
  const re =
    /\bconst\s+([A-Za-z_$][\w$]*)\s*=\s*\$\(\s*([A-Za-z_$][\w$]*)\s*:\s*([A-Za-z_][A-Za-z0-9_]*)/g
  let m
  while ((m = re.exec(source)) !== null) {
    const constName = m[1]
    const innerName = m[2]
    const typeName = m[3]
    if (!SUPPORTED_FIELD_TYPES.has(typeName)) continue
    const fieldName = innerName || constName
    out.set(fieldName, typeName)
  }
  return out
}

function isDataBilditAttrName(attrName) {
  return attrName === 'data-bildit-var-name' || attrName === 'data-bildit-var-type'
}

function jsxOpeningElementHasDataBilditTag(openingEl) {
  if (!openingEl || !Array.isArray(openingEl.attributes)) return false
  return openingEl.attributes.some((a) => {
    if (!t.isJSXAttribute(a)) return false
    if (!t.isJSXIdentifier(a.name)) return false
    return isDataBilditAttrName(a.name.name)
  })
}

function upsertDataBilditAttrs(openingEl, fieldName, fieldType) {
  if (!openingEl || !t.isJSXOpeningElement(openingEl)) return
  if (!Array.isArray(openingEl.attributes)) openingEl.attributes = []

  const attrs = openingEl.attributes
  const next = []
  let sawName = false
  let sawType = false

  for (const a of attrs) {
    if (!t.isJSXAttribute(a) || !t.isJSXIdentifier(a.name)) {
      next.push(a)
      continue
    }
    const n = a.name.name
    if (n === 'data-bildit-var-name') {
      sawName = true
      next.push(t.jsxAttribute(t.jsxIdentifier(n), t.stringLiteral(fieldName)))
      continue
    }
    if (n === 'data-bildit-var-type') {
      sawType = true
      next.push(t.jsxAttribute(t.jsxIdentifier(n), t.stringLiteral(fieldType)))
      continue
    }
    next.push(a)
  }

  if (!sawName) next.push(t.jsxAttribute(t.jsxIdentifier('data-bildit-var-name'), t.stringLiteral(fieldName)))
  if (!sawType) next.push(t.jsxAttribute(t.jsxIdentifier('data-bildit-var-type'), t.stringLiteral(fieldType)))
  openingEl.attributes = next
}

function rootIdentifierName(expr) {
  if (!expr) return null
  if (t.isIdentifier(expr)) return expr.name
  if (t.isTSAsExpression(expr)) return rootIdentifierName(expr.expression)
  if (t.isTSTypeAssertion(expr)) return rootIdentifierName(expr.expression)
  if (t.isParenthesizedExpression(expr)) return rootIdentifierName(expr.expression)
  if (t.isMemberExpression(expr)) return rootIdentifierName(expr.object)
  if (t.isOptionalMemberExpression && t.isOptionalMemberExpression(expr)) return rootIdentifierName(expr.object)
  if (typeof t.isChainExpression === 'function' && t.isChainExpression(expr)) {
    return rootIdentifierName(expr.expression)
  }
  return null
}

/**
 * Collect top-level template field identifiers referenced by an expression:
 * - `sectionTitle.text` → `sectionTitle`
 * - `ctaUrl` → `ctaUrl`
 */
function unwrapOptionalCallCallee(callee) {
  if (!callee) return callee
  if (typeof t.isChainExpression === 'function' && t.isChainExpression(callee)) {
    return callee.expression
  }
  return callee
}

/**
 * When the expression is `receiver.map(fn)` / `flatMap` / `filter`, returns the first
 * function argument; otherwise null.
 */
function getArrayMethodCallback(callExpr) {
  if (!t.isCallExpression(callExpr)) return null
  const callee = unwrapOptionalCallCallee(callExpr.callee)
  if (!t.isMemberExpression(callee) || callee.computed) return null
  if (!t.isIdentifier(callee.property)) return null
  const method = callee.property.name
  if (method !== 'map' && method !== 'flatMap' && method !== 'filter') return null
  const a0 = callExpr.arguments[0]
  if (t.isArrowFunctionExpression(a0) || t.isFunctionExpression(a0)) return a0
  return null
}

/**
 * Template field refs that appear on the callee/receiver or on non-callback arguments
 * only — not inside the `.map` callback body.
 */
function collectTemplateRefsOutsideMapCallback(callExpr, knownFields) {
  const cb = getArrayMethodCallback(callExpr)
  if (!cb) return null
  const callee = unwrapOptionalCallCallee(callExpr.callee)
  const names = new Set()
  for (const n of collectTemplateFieldRefsFromExpression(callee, knownFields)) names.add(n)
  for (let i = 0; i < callExpr.arguments.length; i++) {
    const arg = callExpr.arguments[i]
    if (arg === cb) continue
    for (const n of collectTemplateFieldRefsFromExpression(arg, knownFields)) names.add(n)
  }
  return names
}

/**
 * If the only refs in this expression live inside a `.map`/… callback, tagging the
 * nearest JSX owner (often `<ul>{items.map(...)}</ul>`) mis-attributes clicks on
 * static list text to that field (e.g. a className field used only inside the map).
 */
function shouldSkipOwnerTagForMapExpression(expr, refs, knownFields) {
  if (!expr || !refs || refs.size !== 1) return false
  if (!getArrayMethodCallback(expr)) return false
  const outside = collectTemplateRefsOutsideMapCallback(expr, knownFields)
  if (!outside || outside.size > 0) return false
  return true
}

function collectTemplateFieldRefsFromExpression(expr, knownFields) {
  const names = new Set()
  if (!expr || !knownFields || knownFields.size === 0) return names

  const visit = (node) => {
    if (!node) return
    if (t.isIdentifier(node)) {
      if (knownFields.has(node.name)) names.add(node.name)
      return
    }
    const root = rootIdentifierName(node)
    if (root && knownFields.has(root)) {
      names.add(root)
      return
    }
    // Walk children.
    for (const key of Object.keys(node)) {
      const v = node[key]
      if (!v) continue
      if (Array.isArray(v)) {
        for (const item of v) {
          if (item && typeof item.type === 'string') visit(item)
        }
      } else if (v && typeof v.type === 'string') {
        visit(v)
      }
    }
  }

  visit(expr)
  return names
}

function findNearestOwningJsxOpeningElement(p) {
  let cur = p
  while (cur) {
    if (cur.isJSXElement && cur.isJSXElement()) {
      const oe = cur.node.openingElement
      if (t.isJSXOpeningElement(oe)) return oe
    }
    if (cur.isJSXFragment && cur.isJSXFragment()) return null
    cur = cur.parentPath
  }
  return null
}

function extractV2JsdocFieldTypesFromAst(ast, traverse) {
  const out = new Map()
  traverse(ast, {
    VariableDeclaration(p) {
      if (p.node.kind !== 'const') return
      const typeName = fieldTypeFromLeadingComments(p.node)
      if (!typeName) return
      for (const d of p.node.declarations || []) {
        if (!t.isVariableDeclarator(d)) continue
        if (!t.isIdentifier(d.id)) continue
        out.set(d.id.name, typeName)
      }
    },
    VariableDeclarator(p) {
      if (!t.isIdentifier(p.node.id)) return
      const typeName = fieldTypeFromLeadingComments(p.node)
      if (!typeName) return
      out.set(p.node.id.name, typeName)
    },
  })
  return out
}

/**
 * Add `data-bildit-var-name` + `data-bildit-var-type` to JSX opening elements that
 * directly render a template field (RichText, String, Image, Link, Color — not Boolean, etc.).
 *
 * Rule: if a JSX element references exactly 1 known template field identifier anywhere
 * in an expression container or attribute expression, tag that element.
 *
 * @param {object} args
 * @param {*} args.ast
 * @param {*} args.traverse
 * @param {string} args.source
 * @param {Map<string,string>} args.autoFieldTypes - from codemod-generated fields (fieldRecords)
 */
function tagTemplateVariableUsages({ ast, traverse, source, autoFieldTypes }) {
  const fieldTypes = new Map()
  if (autoFieldTypes && autoFieldTypes.size) {
    for (const [k, v] of autoFieldTypes.entries()) fieldTypes.set(k, v)
  }
  for (const [k, v] of extractLegacyFieldTypesFromSource(source).entries()) fieldTypes.set(k, v)
  for (const [k, v] of extractV2JsdocFieldTypesFromAst(ast, traverse).entries()) fieldTypes.set(k, v)

  if (fieldTypes.size === 0) return { taggedCount: 0, fieldTypes }

  const known = new Set(fieldTypes.keys())
  let taggedCount = 0

  traverse(ast, {
    JSXExpressionContainer(p) {
      const expr = p.node.expression
      if (!expr) return
      const refs = collectTemplateFieldRefsFromExpression(expr, known)
      if (refs.size !== 1) return

      const fieldName = Array.from(refs)[0]
      const fieldType = fieldTypes.get(fieldName)
      if (!fieldType) return
      if (!fieldTypeGetsVisualUsageTags(fieldType)) return

      if (shouldSkipOwnerTagForMapExpression(expr, refs, known)) return

      const openingEl = findNearestOwningJsxOpeningElement(p.parentPath)
      if (!openingEl) return
      if (jsxOpeningElementHasDataBilditTag(openingEl)) return

      upsertDataBilditAttrs(openingEl, fieldName, fieldType)
      taggedCount += 1
    },
    JSXAttribute(p) {
      const v = p.node.value
      if (!t.isJSXExpressionContainer(v)) return
      const expr = v.expression
      const refs = collectTemplateFieldRefsFromExpression(expr, known)
      if (refs.size !== 1) return

      // Don't tag the element because it's already being explicitly tagged.
      if (t.isJSXIdentifier(p.node.name) && isDataBilditAttrName(p.node.name.name)) return

      const fieldName = Array.from(refs)[0]
      const fieldType = fieldTypes.get(fieldName)
      if (!fieldType) return
      if (!fieldTypeGetsVisualUsageTags(fieldType)) return

      if (shouldSkipOwnerTagForMapExpression(expr, refs, known)) return

      const openingEl = findNearestOwningJsxOpeningElement(p.parentPath)
      if (!openingEl) return
      if (jsxOpeningElementHasDataBilditTag(openingEl)) return

      upsertDataBilditAttrs(openingEl, fieldName, fieldType)
      taggedCount += 1
    },
  })

  return { taggedCount, fieldTypes }
}

module.exports = {
  tagTemplateVariableUsages,
  extractLegacyFieldTypesFromSource,
  extractV2JsdocFieldTypesFromAst,
  shouldSkipOwnerTagForMapExpression,
  getArrayMethodCallback,
}

