import { existsSync, statSync } from 'node:fs'
import path from 'node:path'
import generateModule from '@babel/generator'
import { parse } from '@babel/parser'
import type { NodePath } from '@babel/traverse'
import * as traverseModule from '@babel/traverse'
import * as t from '@babel/types'
import {
  type ComponentDoc,
  parse as docgenParse,
} from 'react-docgen-typescript'
import { generateID } from './shared'

// biome-ignore format: Broken the import
const traverse = (traverseModule.default as any).default as typeof import(
  '@babel/traverse'
).default

// biome-ignore format: Broken the import
const generate = (generateModule as any).default as typeof import(
  '@babel/generator'
).default

export async function babelProcessCode(
  filePath: string,
  fileContent: string,
): Promise<{
  metaObject: Based.UI.Extractor.MetaDescriptorResult | null
  blocksArray: Based.UI.Extractor.BlocksDescriptorResult[] | null
}> {
  const ast = parse(fileContent, {
    sourceType: 'module',
    plugins: ['typescript', 'jsx'],
  })

  let metaObject = {} as Based.UI.Extractor.MetaDescriptorResult
  let blocksArray = [] as Based.UI.Extractor.BlocksDescriptorResult[]
  const importMap: Record<string, string> = {}
  let isDefaultExport = false

  traverse(ast, {
    ImportDeclaration(path: NodePath<t.ImportDeclaration>) {
      const importPath = path.node.source.value

      path.node.specifiers.forEach((specifier) => {
        if (
          t.isImportSpecifier(specifier) ||
          t.isImportDefaultSpecifier(specifier) ||
          t.isImportNamespaceSpecifier(specifier)
        ) {
          const localName = specifier.local.name
          importMap[localName] = importPath
        }
      })
    },
    ExportDefaultDeclaration() {
      isDefaultExport = true
    },
    ExportNamedDeclaration(path: NodePath<t.ExportNamedDeclaration>) {
      const { declaration } = path.node

      if (declaration && t.isVariableDeclaration(declaration)) {
        declaration.declarations.forEach((declarator) => {
          if (t.isIdentifier(declarator.id) && declarator.init) {
            if (declarator.id.name === 'meta') {
              metaObject = processNode(declarator.init, importMap, filePath)
            }
            if (declarator.id.name === 'blocks') {
              blocksArray = processNode(declarator.init, importMap, filePath, {
                fileContent,
              })
            }
          }
        })
      }
    },
  })

  if (metaObject && !metaObject.id && metaObject.name) {
    metaObject.id = generateID(metaObject.name)
  }

  if (Array.isArray(blocksArray)) {
    blocksArray = blocksArray.map((block: any) => {
      if (!block.id && block.name) {
        block.id = generateID(block.name)
      }

      return block
    })
  }

  if (metaObject) {
    metaObject = {
      ...metaObject,
      exportType: isDefaultExport ? 'default' : 'named',
    }
  }

  return {
    metaObject,
    blocksArray,
  }
}

function processNode(
  node: t.Node,
  importMap: Record<string, string>,
  currentFilePath: string,
  context: { parentKey?: string; showApi?: boolean; fileContent?: string } = {},
): any {
  let result: any = null

  if (t.isObjectExpression(node)) {
    result = processObject(
      node,
      importMap,
      currentFilePath,
      context.fileContent,
    )
  } else if (t.isArrayExpression(node)) {
    result = processArray(
      node,
      importMap,
      currentFilePath,
      context.fileContent!,
    )
  } else if (
    t.isArrowFunctionExpression(node) ||
    t.isFunctionExpression(node)
  ) {
    result = processFunction(node)
  } else if (
    t.isStringLiteral(node) ||
    t.isNumericLiteral(node) ||
    t.isBooleanLiteral(node)
  ) {
    result = node.value
  } else if (t.isIdentifier(node)) {
    if (context.parentKey === 'component' && context.showApi) {
      const componentName = node.name
      const importPath = importMap[componentName]

      if (importPath) {
        const absoluteImportPath = resolveImportPath(
          importPath,
          componentName,
          currentFilePath,
        )
        if (absoluteImportPath) {
          try {
            const componentInfo: ComponentDoc[] = docgenParse(
              absoluteImportPath,
              {
                savePropValueAsString: true,
              },
            )
            result = findComponentByDisplayName(componentInfo, componentName)
          } catch (error) {
            console.error(
              `Error processing component "${componentName}" from ${absoluteImportPath}`,
              error,
            )
            result = null
          }
        }
      }
    } else {
      result = node.name
    }
  }

  return result
}

function processObject(
  node: t.ObjectExpression,
  importMap: Record<string, string>,
  currentFilePath: string,
  fileContent?: string,
): any {
  const obj: Record<string, any> = {}
  let showApi = true

  node.properties.forEach((prop) => {
    if (
      t.isObjectProperty(prop) &&
      t.isIdentifier(prop.key) &&
      prop.key.name === 'showApi' &&
      t.isBooleanLiteral(prop.value)
    ) {
      showApi = prop.value.value
    }
  })

  node.properties.forEach((prop) => {
    if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
      const key = prop.key.name
      const hasCode = key === 'code'
      const isRender = key === 'render'

      if (!hasCode && isRender) {
        obj.code = extractCodeFromNode(prop.value, fileContent)
      }

      obj[key] = processNode(prop.value, importMap, currentFilePath, {
        parentKey: key,
        showApi,
        fileContent,
      })
    }
  })

  return obj
}

function extractCodeFromNode(node: t.Node, fileContent?: string) {
  if (fileContent && node.start && node.end) {
    return fileContent.slice(node.start, node.end)
  }
  return null
}

function processArray(
  node: t.ArrayExpression,
  importMap: Record<string, string>,
  currentFilePath: string,
  fileContent: string,
): any[] {
  const result = node.elements.map((element) => {
    if (element) {
      return processNode(element, importMap, currentFilePath, { fileContent })
    }

    return null
  })

  return result
}

function processFunction(
  node: t.ArrowFunctionExpression | t.FunctionExpression,
): string {
  const { code } = generate(node, {
    compact: false,
    concise: false,
    jsescOption: {
      quotes: 'single',
      compact: false,
    },
  })

  return code
}

function findComponentByDisplayName(
  components: ComponentDoc[],
  componentName: string,
): ComponentDoc | null {
  return (
    components.find((component) => component.displayName === componentName) ||
    null
  )
}

export function resolveImportPath(
  importPath: string,
  componentName: string,
  currentFilePath: string,
): string | null {
  if (importPath.startsWith('.')) {
    const dir = path.dirname(currentFilePath)
    const resolvedPath = path.resolve(dir, importPath)
    if (existsSync(resolvedPath)) {
      if (statSync(resolvedPath).isDirectory()) {
        const indexTsx = path.join(resolvedPath, 'index.tsx')
        const indexTs = path.join(resolvedPath, 'index.ts')
        if (existsSync(indexTsx)) {
          return indexTsx
        }
        if (existsSync(indexTs)) {
          return indexTs
        }
      } else {
        return resolvedPath
      }
    }
    const extensions = ['.tsx', '.ts', '.jsx', '.js']
    for (const ext of extensions) {
      if (existsSync(`${resolvedPath}${ext}`)) {
        return `${resolvedPath}${ext}`
      }
    }
  } else {
    try {
      const specificPath = `${importPath}/components/${componentName}`
      const resolved = require.resolve(specificPath, {
        paths: [path.dirname(currentFilePath)],
      })
      if (resolved.includes('/dist/')) {
        const srcPath = resolved
          .replace('/dist/', '/src/')
          .replace(/\.js$/, '.tsx')
        if (existsSync(srcPath)) {
          return srcPath
        }
      }
      return resolved
    } catch {
      try {
        const resolved = require.resolve(importPath, {
          paths: [path.dirname(currentFilePath)],
        })
        if (resolved.includes('/dist/')) {
          const srcPath = resolved
            .replace('/dist/', '/src/')
            .replace(/\.js$/, '.tsx')
          if (existsSync(srcPath)) {
            return srcPath
          }
        }
        return resolved
      } catch {
        return null
      }
    }
  }
  return null
}
