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

/**
 * Convert a CSS declaration block into an ObjectExpression suitable for a React
 * style prop. Returns { objectExpression, skipped } where skipped is a list of
 * declarations we could not represent (e.g. !important, custom properties).
 */
function cssDeclarationsToStyleObject(declarations) {
  const properties = []
  const skipped = []
  for (const decl of declarations) {
    if (decl.important) {
      skipped.push({ prop: decl.prop, reason: 'important' })
      continue
    }
    if (decl.prop.startsWith('--')) {
      skipped.push({ prop: decl.prop, reason: 'custom-property' })
      continue
    }
    const key = cssPropToCamelCase(decl.prop)
    properties.push(
      t.objectProperty(t.identifier(key), t.stringLiteral(decl.value.trim()))
    )
  }
  return { objectExpression: t.objectExpression(properties), skipped }
}

function cssPropToCamelCase(prop) {
  return prop.replace(/-([a-z])/g, (_, c) => c.toUpperCase())
}

/**
 * Merge an ObjectExpression into an existing JSX style prop (or add one).
 * Later declarations win (we spread the original first, then our new object).
 */
function mergeStyleIntoElement(openingElement, extraObjectExpression) {
  if (!extraObjectExpression.properties.length) return

  const styleAttr = openingElement.attributes.find(
    (a) => t.isJSXAttribute(a) && t.isJSXIdentifier(a.name, { name: 'style' })
  )

  if (!styleAttr) {
    openingElement.attributes.push(
      t.jsxAttribute(
        t.jsxIdentifier('style'),
        t.jsxExpressionContainer(extraObjectExpression)
      )
    )
    return
  }

  if (!t.isJSXExpressionContainer(styleAttr.value)) return
  const expr = styleAttr.value.expression
  if (t.isObjectExpression(expr)) {
    expr.properties.push(...extraObjectExpression.properties)
    return
  }
  styleAttr.value = t.jsxExpressionContainer(
    t.objectExpression([
      t.spreadElement(expr),
      ...extraObjectExpression.properties,
    ])
  )
}

/**
 * Add a className to an element (creates or appends to existing string literal).
 * Intentionally conservative: if className is a JSX expression we leave it alone
 * and the caller should fall back to inline style or <style> tag.
 */
function appendClassName(openingElement, className) {
  const existing = openingElement.attributes.find(
    (a) => t.isJSXAttribute(a) && t.isJSXIdentifier(a.name, { name: 'className' })
  )
  if (!existing) {
    openingElement.attributes.push(
      t.jsxAttribute(t.jsxIdentifier('className'), t.stringLiteral(className))
    )
    return true
  }
  if (t.isStringLiteral(existing.value)) {
    existing.value = t.stringLiteral(`${existing.value.value} ${className}`.trim())
    return true
  }
  return false
}

function hashString(s) {
  let h = 5381
  for (let i = 0; i < s.length; i += 1) {
    h = ((h << 5) + h) ^ s.charCodeAt(i)
  }
  return (h >>> 0).toString(36)
}

function parserPluginsFor(codeType) {
  // Keep this permissive: templates in the wild often include modern syntax, and we
  // want dependency analysis / codemods to degrade gracefully (errorRecovery) rather
  // than failing on parse.
  const plugins = [
    'jsx',
    // Stage-3 / commonly used syntax in Next.js / modern tooling
    'dynamicImport',
    'importMeta',
    'classProperties',
    'classPrivateProperties',
    'classPrivateMethods',
    'objectRestSpread',
    'optionalCatchBinding',
    'topLevelAwait',
    'numericSeparator',
    'logicalAssignment',
    'nullishCoalescingOperator',
    'optionalChaining',
    'exportDefaultFrom',
    'exportNamespaceFrom',
    'throwExpressions',
    'privateIn',
    // Decorators are still common in some codebases; legacy mode is the least risky.
    'decorators-legacy',
    // Import assertions are used by some bundlers (JSON modules etc.)
    'importAssertions',
  ]

  // Be tolerant of TypeScript syntax even in .js/.jsx templates.
  // This fixes cases where templates contain annotations or `as` casts.
  if (codeType === 'ts' || codeType === 'tsx' || codeType === 'jsx') plugins.push('typescript')

  return plugins
}

/**
 * With `errorRecovery: true`, Babel may retain `TSTypeCastExpression` nodes for
 * invalid parenthesized assertions like `(x: string)` in value position.
 * @babel/generator has no printer for that type; map to `TSAsExpression` (`x as string`).
 */
function normalizeTSTypeCastExpressionsForGenerator(ast, traverseFn) {
  traverseFn(ast, {
    enter(path) {
      if (path.node.type !== 'TSTypeCastExpression') return
      const ann = path.node.typeAnnotation
      const inner = ann && ann.typeAnnotation
      if (!inner) return
      const asExpr = t.tsAsExpression(path.node.expression, inner)
      if (path.node.extra) asExpr.extra = { ...path.node.extra }
      path.replaceWith(asExpr)
    },
  })
}

/** True when Babel left a value-position `TSTypeCastExpression` (errorRecovery parse). */
function programHasTSTypeCastExpression(ast, traverseFn) {
  let found = false
  traverseFn(ast, {
    enter(path) {
      if (path.node.type !== 'TSTypeCastExpression') {
        return
      }
      found = true
      path.stop()
    },
  })
  return found
}

module.exports = {
  cssDeclarationsToStyleObject,
  cssPropToCamelCase,
  mergeStyleIntoElement,
  appendClassName,
  hashString,
  parserPluginsFor,
  normalizeTSTypeCastExpressionsForGenerator,
  programHasTSTypeCastExpression,
}
