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

function isPascalCaseComponentName(name) {
  return typeof name === 'string' && /^[A-Z][A-Za-z0-9]*$/.test(name)
}

/**
 * Resolve the function path for the component: default export, or first named
 * `export function Foo` / `export const Foo = ()` with PascalCase Foo.
 */
function findPrimaryComponentFunctionPath(ast, traverse) {
  let fnPath = null

  function resolveFromExportDefault(p) {
    const d = p.node.declaration
    if (t.isFunctionDeclaration(d)) return p.get('declaration')
    if (t.isArrowFunctionExpression(d)) return p.get('declaration')
    if (t.isFunctionExpression(d)) return p.get('declaration')
    if (t.isIdentifier(d)) {
      const binding = p.scope.getBinding(d.name)
      if (!binding) return null
      if (binding.path.isFunctionDeclaration()) return binding.path
      if (binding.path.isVariableDeclarator()) {
        const init = binding.path.get('init')
        if (init.isArrowFunctionExpression() || init.isFunctionExpression()) return init
      }
    }
    return null
  }

  traverse(ast, {
    ExportDefaultDeclaration(p) {
      fnPath = resolveFromExportDefault(p)
      if (fnPath) p.stop()
    },
  })
  if (fnPath) return fnPath

  traverse(ast, {
    ExportNamedDeclaration(p) {
      const decl = p.node.declaration
      if (t.isFunctionDeclaration(decl) && decl.id && isPascalCaseComponentName(decl.id.name)) {
        fnPath = p.get('declaration')
        p.stop()
        return
      }
      if (t.isVariableDeclaration(decl)) {
        const vdPath = p.get('declaration')
        for (let i = 0; i < decl.declarations.length; i += 1) {
          const declPath = vdPath.get(`declarations.${i}`)
          const id = declPath.node.id
          if (!t.isIdentifier(id) || !isPascalCaseComponentName(id.name)) continue
          const initPath = declPath.get('init')
          if (initPath.isArrowFunctionExpression() || initPath.isFunctionExpression()) {
            fnPath = initPath
            p.stop()
            return
          }
        }
      }
    },
  })

  return fnPath
}

/**
 * True if the module already exposes a default binding (including `export { X as default }`).
 */
function programHasDefaultExport(ast, traverse) {
  let has = false
  traverse(ast, {
    ExportDefaultDeclaration(p) {
      has = true
      p.stop()
    },
    ExportNamedDeclaration(p) {
      for (const s of p.node.specifiers) {
        if (t.isExportSpecifier(s) && t.isIdentifier(s.exported) && s.exported.name === 'default') {
          has = true
          p.stop()
          return
        }
      }
    },
  })
  return has
}

function primaryComponentBindingName(fnPath) {
  if (!fnPath) return null
  if (fnPath.isFunctionDeclaration() && fnPath.node.id) return fnPath.node.id.name
  if (fnPath.isArrowFunctionExpression() || fnPath.isFunctionExpression()) {
    const parent = fnPath.parentPath
    if (parent && parent.isVariableDeclarator() && t.isIdentifier(parent.node.id)) {
      return parent.node.id.name
    }
  }
  return null
}

/**
 * If the file has no default export but has a primary PascalCase component (named export),
 * append `export default Name` so templates match CMS expectations.
 * @returns {boolean} whether a new export was added
 */
function ensureDefaultExport(ast, traverse) {
  if (programHasDefaultExport(ast, traverse)) return false

  const fnPath = findPrimaryComponentFunctionPath(ast, traverse)
  const name = primaryComponentBindingName(fnPath)
  if (!name || !isPascalCaseComponentName(name)) return false

  let programPath = null
  traverse(ast, {
    Program(p) {
      programPath = p
      p.stop()
    },
  })
  if (!programPath) return false

  programPath.pushContainer('body', t.exportDefaultDeclaration(t.identifier(name)))
  return true
}

function normalizeArrowFunctionToBlock(fnPath) {
  if (
    fnPath &&
    fnPath.isArrowFunctionExpression() &&
    !t.isBlockStatement(fnPath.node.body)
  ) {
    const body = fnPath.node.body
    fnPath.node.body = t.blockStatement([t.returnStatement(body)])
  }
}

/**
 * Find the root JSX element path inside the primary component's return
 * (default export, or named `export function Component` / `export const Component = …`).
 */
function findReturnRootJsxPath(ast, traverse) {
  let jsxPath = null
  function visitFunction(fnPath) {
    if (jsxPath || !fnPath) return
    const body = fnPath.node.body
    if (t.isBlockStatement(body)) {
      fnPath.traverse({
        ReturnStatement(p) {
          if (jsxPath) return
          let arg = p.node.argument
          if (t.isParenthesizedExpression(arg)) arg = arg.expression
          if (t.isJSXElement(arg)) {
            jsxPath = t.isParenthesizedExpression(p.node.argument)
              ? p.get('argument.expression')
              : p.get('argument')
          }
        },
      })
    } else if (t.isJSXElement(body)) {
      jsxPath = fnPath.get('body')
    }
  }

  const fnPath = findPrimaryComponentFunctionPath(ast, traverse)
  if (fnPath) {
    normalizeArrowFunctionToBlock(fnPath)
    visitFunction(fnPath)
  }
  return jsxPath
}

/**
 * Map a field anchor path to a group index (direct child index of root JSX), or -1 for root/general.
 */
function findGroupIndex(rootPath, startPath) {
  if (!rootPath || !startPath) return -1
  let el = startPath
  if (el.isJSXText()) el = el.parentPath
  if (el.isJSXAttribute()) el = el.parentPath.parentPath
  while (el && !el.isJSXElement()) el = el.parentPath
  if (!el) return -1
  if (el.node === rootPath.node) return -1
  let cur = el
  while (cur) {
    const parent = cur.parentPath
    if (!parent) return -1
    if (parent.isJSXElement()) {
      if (parent.node === rootPath.node) {
        const children = parent.node.children
        for (let i = 0; i < children.length; i += 1) {
          if (children[i] === cur.node) return i
        }
        return -1
      }
      cur = parent
    } else {
      break
    }
  }
  return -1
}

function groupLabelFromChild(rootPath, childIndex) {
  if (childIndex < 0) return 'General'
  const children = rootPath.node.children
  const child = children[childIndex]
  if (!child) return `Section ${childIndex + 1}`
  if (t.isJSXElement(child) && t.isJSXIdentifier(child.openingElement.name)) {
    return child.openingElement.name.name
  }
  return `Section ${childIndex + 1}`
}

function formatGroupStartLineComments(index, label) {
  return ` group { ${index}. ${label} }`
}

function formatGroupEndLineComments() {
  return ' endgroup'
}

/** Maps codemod field kind to @type line in v2 JSDoc (Belk / template library style). */
function mapKindToJsdocType(kind) {
  switch (kind) {
    case 'RichText':
      return 'RichText'
    case 'Color':
      return 'Color'
    case 'String':
      return 'String'
    case 'Boolean':
      return 'Boolean'
    case 'Number':
      return 'Number'
    case 'Image':
      return 'Image'
    case 'Link':
      return 'Link'
    default:
      return 'String'
  }
}

/**
 * Inner text for a CommentBlock (without block-comment delimiters).
 * Matches hand-written v2 templates: @group + @type per const.
 */
function formatV2ConstJsdocBlock(groupLabel, fieldKind) {
  const typeName = mapKindToJsdocType(fieldKind)
  const safeLabel = String(groupLabel).replace(/\s+/g, ' ').trim() || 'General'
  return `*\n * @group ${safeLabel}\n * @type ${typeName}\n `
}

/**
 * Ensure the primary component (default or named PascalCase export) has a block body;
 * return its BlockStatement path for inserting const declarations.
 * @returns {import('@babel/traverse').NodePath | null}
 */
function ensureBlockBody(ast, traverse) {
  const fnPath = findPrimaryComponentFunctionPath(ast, traverse)
  if (!fnPath) return null
  normalizeArrowFunctionToBlock(fnPath)
  if (
    (fnPath.isFunctionDeclaration() ||
      fnPath.isFunctionExpression() ||
      fnPath.isArrowFunctionExpression()) &&
    t.isBlockStatement(fnPath.node.body)
  ) {
    return fnPath.get('body')
  }
  return null
}

/**
 * @param {import('@babel/traverse').NodePath} fnPath
 * @returns {string | null} PascalCase name, or null if anonymous/unsupported
 */
function pascalNameFromFunctionPath(fnPath) {
  if (!fnPath) return null
  if (fnPath.isFunctionDeclaration() && fnPath.node.id) {
    return fnPath.node.id.name
  }
  if (fnPath.isFunctionExpression() || fnPath.isArrowFunctionExpression()) {
    const p = fnPath.parentPath
    if (p && p.isVariableDeclarator() && t.isIdentifier(p.node.id)) {
      return p.node.id.name
    }
  }
  return null
}

/**
 * Walks up from any path to the nearest function scope (incl. arrow, class method).
 * @param {import('@babel/traverse').NodePath} path
 */
function findEnclosingFunctionPath(path) {
  let cur = path
  while (cur) {
    if (
      cur.isFunctionDeclaration() ||
      cur.isFunctionExpression() ||
      cur.isArrowFunctionExpression() ||
      (typeof cur.isObjectMethod === 'function' && cur.isObjectMethod()) ||
      (typeof cur.isClassMethod === 'function' && cur.isClassMethod())
    ) {
      return cur
    }
    cur = cur.parentPath
  }
  return null
}

/**
 * Block body to insert per-function field hoists. Used for nested function components
 * (helper JSX) so $() consts are not only placed on the primary component.
 * @param {import('@babel/types').File} _ast
 * @param {typeof import('@babel/traverse')['default']} traverse
 * @param {string} name
 */
function findFunctionBodyBlockByName(_ast, traverse, name) {
  if (!name) return null
  let blockPath = null
  traverse(_ast, {
    FunctionDeclaration(p) {
      if (p.node.id && p.node.id.name === name) {
        normalizeArrowFunctionToBlock(p)
        if (t.isBlockStatement(p.node.body)) {
          blockPath = p.get('body')
        }
        p.stop()
      }
    },
  })
  if (blockPath) return blockPath

  traverse(_ast, {
    VariableDeclarator(p) {
      if (!t.isIdentifier(p.node.id) || p.node.id.name !== name) return
      const init = p.get('init')
      if (init.isArrowFunctionExpression() || init.isFunctionExpression()) {
        normalizeArrowFunctionToBlock(init)
        if (t.isBlockStatement(init.node.body)) {
          blockPath = init.get('body')
          p.stop()
        }
      }
    },
  })
  return blockPath
}

/**
 * Insert grouped const declarations at the top of the block.
 * @param {(fieldName: string, kind: string, defaultValue: *) => import('@babel/types').Expression} buildInitializer
 *        — templatePlaceholder (legacy → $()) or literalFieldInitializer (v2 plain JS).
 * @param {Array<{ fieldName: string, kind: string, defaultValue: unknown, groupIndex: number, hoistFunctionName?: string | null }>} fieldRecords
 *        `hoistFunctionName` (PascalCase) — when set, the const is inserted at the top of that
 *        function body instead of the primary template component. Must match
 *        `nestingPascalName` in tagTemplateFields.
 */
function hoistFieldDeclarations(ast, traverse, fieldRecords, groupingStyle, buildInitializer) {
  if (!fieldRecords || !fieldRecords.length) return

  let programPath = null
  traverse(ast, {
    Program(p) {
      programPath = p
      p.stop()
    },
  })
  if (!programPath) return

  const byKey = new Map()
  for (const rec of fieldRecords) {
    if (!byKey.has(rec.fieldName)) byKey.set(rec.fieldName, rec)
  }
  const unique = Array.from(byKey.values())

  unique.sort((a, b) => {
    const aNest = a.hoistFunctionName || ''
    const bNest = b.hoistFunctionName || ''
    if (aNest !== bNest) return aNest.localeCompare(bNest)
    const ga = a.groupIndex === undefined ? -1 : a.groupIndex
    const gb = b.groupIndex === undefined ? -1 : b.groupIndex
    if (ga !== gb) return ga - gb
    return a.fieldName.localeCompare(b.fieldName)
  })

  const rootPath = findReturnRootJsxPath(ast, traverse)

  const groups = []
  let currentG = null
  for (const rec of unique) {
    const groupKey = `g:${rec.groupIndex === undefined ? -1 : rec.groupIndex}`
    if (!currentG || currentG.key !== groupKey) {
      const label = groupLabelFromChild(rootPath, rec.groupIndex === undefined ? -1 : rec.groupIndex)
      currentG = { key: groupKey, label, fields: [] }
      groups.push(currentG)
    }
    currentG.fields.push(rec)
  }

  const newStmts = []
  groups.forEach((g, groupIdx) => {
    const label = g.label
    const displayIndex = groupIdx + 1

    for (let i = 0; i < g.fields.length; i += 1) {
      const rec = g.fields[i]
      const init = buildInitializer(rec.fieldName, rec.kind, rec.defaultValue)
      const decl = t.variableDeclaration('const', [
        t.variableDeclarator(t.identifier(rec.fieldName), init),
      ])

      const isFirst = i === 0
      const isLast = i === g.fields.length - 1

      if (groupingStyle === 'lineComments') {
        if (isFirst) {
          decl.leadingComments = [
            { type: 'CommentLine', value: formatGroupStartLineComments(displayIndex, label) },
          ]
        }
        if (isLast) {
          decl.trailingComments = [
            ...(decl.trailingComments || []),
            { type: 'CommentLine', value: formatGroupEndLineComments() },
          ]
        }
      } else if (groupingStyle === 'jsdoc') {
        decl.leadingComments = [
          {
            type: 'CommentBlock',
            value: formatV2ConstJsdocBlock(label, rec.kind),
          },
        ]
      }

      newStmts.push(decl)
    }
  })

  const bodyPaths = programPath.get('body')
  let insertIdx = 0
  while (insertIdx < bodyPaths.length) {
    const p = bodyPaths[insertIdx]
    if (p.isImportDeclaration()) {
      insertIdx += 1
      continue
    }
    if (p.isExpressionStatement() && t.isStringLiteral(p.node.expression)) {
      insertIdx += 1
      continue
    }
    break
  }

  if (insertIdx >= bodyPaths.length) {
    programPath.pushContainer('body', newStmts)
    return
  }

  const anchor = bodyPaths[insertIdx]
  for (let i = newStmts.length - 1; i >= 0; i -= 1) {
    anchor.insertBefore(newStmts[i])
  }
}

module.exports = {
  findPrimaryComponentFunctionPath,
  findReturnRootJsxPath,
  findGroupIndex,
  pascalNameFromFunctionPath,
  findEnclosingFunctionPath,
  findFunctionBodyBlockByName,
  hoistFieldDeclarations,
  groupLabelFromChild,
  formatV2ConstJsdocBlock,
  formatGroupStartLineComments,
  formatGroupEndLineComments,
  programHasDefaultExport,
  ensureDefaultExport,
}
