const path = require('path')
const parser = require('@babel/parser')
const traverseMod = require('@babel/traverse')
const t = require('@babel/types')

const traverse = traverseMod.default || traverseMod

const { parserPluginsFor } = require('./codemod/utils')

const DEFERRED_HOOKS = new Set([
  'useEffect',
  'useLayoutEffect',
  'useInsertionEffect',
  'useCallback',
])

const BROWSER_GLOBAL_IDS = new Set([
  'window',
  'document',
  'localStorage',
  'sessionStorage',
  'navigator',
  'location',
])

/** @param {import('@babel/traverse').NodePath} path */
function isDescendantOf(path, ancestorPath) {
  if (!ancestorPath || !path) return false
  let p = path
  while (p) {
    if (p === ancestorPath) return true
    p = p.parentPath
  }
  return false
}

function getCalleeName(callee) {
  if (t.isIdentifier(callee)) return callee.name
  if (t.isMemberExpression(callee) && !callee.computed && t.isIdentifier(callee.property)) {
    return callee.property.name
  }
  return null
}

function isDeferredHookCall(callee) {
  const name = getCalleeName(callee)
  return name && DEFERRED_HOOKS.has(name)
}

/**
 * @param {import('@babel/traverse').NodePath} path
 */
function isBrowserSafeRenderContext(path) {
  let p = path
  while (p) {
    if (p.isCallExpression() && isDeferredHookCall(p.node.callee)) {
      const arg0Path = p.get('arguments.0')
      if (arg0Path.node && isDescendantOf(path, arg0Path)) {
        return true
      }
    }
    p = p.parentPath
  }

  let q = path
  while (q) {
    if (q.isJSXAttribute()) {
      const name = q.node.name
      if (t.isJSXIdentifier(name) && (/^on[A-Z]/.test(name.name) || name.name === 'ref')) {
        const val = q.get('value')
        if (val && val.isJSXExpressionContainer() && isDescendantOf(path, val)) {
          return true
        }
      }
    }
    q = q.parentPath
  }
  return false
}

function detectCodeType(filePath) {
  const ext = path.extname(filePath).toLowerCase()
  if (ext === '.tsx' || ext === '.ts') return 'tsx'
  return 'jsx'
}

/**
 * Parses source and returns SSR/hydration risk sites (browser-only globals in render paths).
 * @param {string} source
 * @param {string} filePath - for parser plugins and display
 * @returns {{ issues: Array<{ line: number, column: number, code: string, hint: string }> }}
 */
function findSsrHydrationRisks(source, filePath) {
  const codeType = detectCodeType(filePath)
  const ast = parser.parse(source, {
    sourceType: 'module',
    plugins: parserPluginsFor(codeType),
    errorRecovery: true,
  })

  const issues = []

  function pushIssue(node, code, hint) {
    if (!node || !node.loc) return
    issues.push({
      line: node.loc.start.line,
      column: node.loc.start.column + 1,
      code,
      hint,
    })
  }

  function maybeFlag(path, node, code, hint) {
    if (isBrowserSafeRenderContext(path)) return
    pushIssue(node, code, hint)
  }

  const visitors = {
    UnaryExpression(p) {
      if (p.node.operator !== 'typeof') return
      const arg = p.node.argument
      if (!t.isIdentifier(arg)) return
      if (arg.name !== 'window' && arg.name !== 'document') return
      maybeFlag(
        p,
        p.node,
        `typeof ${arg.name}`,
        `Avoid typeof ${arg.name} in render; use useEffect (or split client-only UI) so server and first client render match.`,
      )
    },

    MemberExpression(p) {
      const { object, property } = p.node
      if (!t.isIdentifier(object)) return
      if (object.name !== 'window' && object.name !== 'document' && object.name !== 'globalThis') {
        return
      }
      const prop = t.isIdentifier(property) && !p.node.computed ? property.name : null
      const code = prop ? `${object.name}.${prop}` : `${object.name}[…]`
      maybeFlag(
        p,
        p.node,
        code,
        `Move ${object.name} access to useEffect, event handlers, or a client-only child so SSR output matches hydration.`,
      )
    },

    Identifier(p) {
      const { name } = p.node
      if (!BROWSER_GLOBAL_IDS.has(name)) return
      if (p.scope.hasBinding(name)) return
      if (p.parentPath.isMemberExpression({ object: p.node })) return
      if (p.parentPath.isObjectProperty({ key: p.node })) return
      maybeFlag(
        p,
        p.node,
        name,
        name === 'window' || name === 'document'
          ? `Avoid ${name} in render; use useEffect, event handlers, or a client-only subtree so SSR matches the first client render.`
          : `Avoid global ${name} in render; read it in useEffect or behind a stable "mounted" flag from useEffect.`,
      )
    },

    CallExpression(p) {
      const callee = p.node.callee
      if (t.isMemberExpression(callee) && !callee.computed) {
        if (t.isIdentifier(callee.object, { name: 'Math' }) && t.isIdentifier(callee.property, { name: 'random' })) {
          maybeFlag(
            p,
            p.node,
            'Math.random()',
            'Use a stable value for SSR or call Math.random() inside useEffect (not during render).',
          )
          return
        }
        if (t.isIdentifier(callee.object, { name: 'Date' }) && t.isIdentifier(callee.property, { name: 'now' })) {
          maybeFlag(
            p,
            p.node,
            'Date.now()',
            'Use a fixed timestamp or derive time in useEffect so server and client HTML match.',
          )
        }
      }
    },
  }

  traverse(ast, visitors)

  issues.sort((a, b) => a.line - b.line || a.column - b.column)
  return { issues }
}

module.exports = {
  findSsrHydrationRisks,
  DEFERRED_HOOKS,
}
