import { sortBy } from 'es-toolkit'

import { readFileAsync } from './misc.js'

const TOP_LEVEL_NAMESPACE = '__TOP_LEVEL_MODULE__'
export const DEFAULT_TYPE_DEF_HEADER = `/* auto-generated by NAPI-RS */
/* eslint-disable */
`

enum TypeDefKind {
  Const = 'const',
  Enum = 'enum',
  StringEnum = 'string_enum',
  Interface = 'interface',
  Type = 'type',
  Fn = 'fn',
  Struct = 'struct',
  Extends = 'extends',
  Impl = 'impl',
}

interface TypeDefLine {
  kind: TypeDefKind
  name: string
  original_name?: string
  def: string
  extends?: string
  js_doc?: string
  js_mod?: string
}

/**
 * Render a single intermediate type-def line as the TypeScript source it
 * should produce in `index.d.ts`.
 *
 * @param line - The intermediate type-def entry to render.
 * @param constEnum - When true, emit numeric and string `#[napi]` enums as
 *   `const enum`. When false (`--no-const-enum`), numeric enums become
 *   regular runtime enums and string enums fall back to a type-only union
 *   unless `runtimeStringEnum` is also true.
 * @param runtimeStringEnum - When true under `--no-const-enum`, emit
 *   `#[napi(string_enum)]` as a runtime enum (`export declare enum`)
 *   instead of a type-only union. No-op when `constEnum` is true.
 * @param ident - Indentation level applied to the rendered output.
 * @param ambient - When true, emit declarations in the ambient form used
 *   inside `declare namespace` blocks (e.g. drop the `declare` keyword).
 */
function prettyPrint(
  line: TypeDefLine,
  constEnum: boolean,
  runtimeStringEnum: boolean,
  ident: number,
  ambient = false,
): string {
  let s = line.js_doc ?? ''
  switch (line.kind) {
    case TypeDefKind.Interface:
      s += `export interface ${line.name} {\n${line.def}\n}`
      break

    case TypeDefKind.Type:
      s += `export type ${line.name} = \n${line.def}`
      break

    case TypeDefKind.Enum: {
      const enumName = constEnum ? 'const enum' : 'enum'
      s += `${exportDeclare(ambient)} ${enumName} ${line.name} {\n${line.def}\n}`
      break
    }

    case TypeDefKind.StringEnum: {
      if (constEnum) {
        s += `${exportDeclare(ambient)} const enum ${line.name} {\n${line.def}\n}`
      } else if (runtimeStringEnum) {
        s += `${exportDeclare(ambient)} enum ${line.name} {\n${line.def}\n}`
      } else {
        s += `export type ${line.name} = ${line.def.replaceAll(/.*=/g, '').replaceAll(',', '|')};`
      }
      break
    }

    case TypeDefKind.Struct:
      const extendsDef = line.extends ? ` extends ${line.extends}` : ''
      if (line.extends) {
        // Extract generic params from extends type like Iterator<T, TResult, TNext>
        const genericMatch = line.extends.match(/Iterator<(.+)>$/)
        if (genericMatch) {
          const [T, TResult, TNext] = genericMatch[1]
            .split(',')
            .map((p) => p.trim())
          line.def =
            line.def +
            `\nnext(value?: ${TNext}): IteratorResult<${T}, ${TResult}>`
        }
      }
      s += `${exportDeclare(ambient)} class ${line.name}${extendsDef} {\n${line.def}\n}`
      if (line.original_name && line.original_name !== line.name) {
        s += `\nexport type ${line.original_name} = ${line.name}`
      }
      break

    case TypeDefKind.Fn:
      s += `${exportDeclare(ambient)} ${line.def}`
      break

    default:
      s += line.def
  }

  return correctStringIdent(s, ident)
}

function exportDeclare(ambient: boolean): string {
  if (ambient) {
    return 'export'
  }

  return 'export declare'
}

/**
 * Read the napi-derive-emitted intermediate type-def file and render its
 * entries into the `index.d.ts` source string plus the list of names to
 * re-export from `index.js`.
 *
 * @param intermediateTypeFile - Path to the JSONL type-def file produced
 *   by napi-derive (one entry per `#[napi]` item).
 * @param constEnum - See {@link prettyPrint}.
 * @param runtimeStringEnum - See {@link prettyPrint}. Defaults to `false`.
 */
export async function processTypeDef(
  intermediateTypeFile: string,
  constEnum: boolean,
  runtimeStringEnum: boolean = false,
) {
  const exports: string[] = []
  const defs = await readIntermediateTypeFile(intermediateTypeFile)
  const groupedDefs = preprocessTypeDef(defs)

  const dts =
    sortBy(Array.from(groupedDefs), [([namespace]) => namespace])
      .map(([namespace, defs]) => {
        if (namespace === TOP_LEVEL_NAMESPACE) {
          return defs
            .map((def) => {
              switch (def.kind) {
                case TypeDefKind.Const:
                case TypeDefKind.Enum:
                case TypeDefKind.StringEnum:
                case TypeDefKind.Fn:
                case TypeDefKind.Struct: {
                  exports.push(def.name)
                  if (def.original_name && def.original_name !== def.name) {
                    exports.push(def.original_name)
                  }
                  break
                }
                default:
                  break
              }
              return prettyPrint(def, constEnum, runtimeStringEnum, 0)
            })
            .join('\n\n')
        } else {
          exports.push(namespace)
          let declaration = ''
          declaration += `export declare namespace ${namespace} {\n`
          for (const def of defs) {
            declaration +=
              prettyPrint(def, constEnum, runtimeStringEnum, 2, true) + '\n'
          }
          declaration += '}'
          return declaration
        }
      })
      .join('\n\n') + '\n'

  return {
    dts,
    exports,
  }
}

async function readIntermediateTypeFile(file: string) {
  const content = await readFileAsync(file, 'utf8')

  const defs = content
    .split('\n')
    .filter(Boolean)
    .map((line) => {
      line = line.trim()
      const parsed = JSON.parse(line) as TypeDefLine
      // Convert escaped newlines back to actual newlines in js_doc fields
      if (parsed.js_doc) {
        parsed.js_doc = parsed.js_doc.replace(/\\n/g, '\n')
      }
      // Convert escaped newlines to actual newlines in def fields for struct/class/interface/type types
      // where \n represents method/field separators that should be actual newlines
      if (parsed.def) {
        parsed.def = parsed.def.replace(/\\n/g, '\n')
      }
      return parsed
    })

  // move all `struct` def to the very top
  // and order the rest alphabetically.
  return defs.sort((a, b) => {
    if (a.kind === TypeDefKind.Struct) {
      if (b.kind === TypeDefKind.Struct) {
        return a.name.localeCompare(b.name)
      }
      return -1
    } else if (b.kind === TypeDefKind.Struct) {
      return 1
    } else {
      return a.name.localeCompare(b.name)
    }
  })
}

function preprocessTypeDef(defs: TypeDefLine[]): Map<string, TypeDefLine[]> {
  const namespaceGrouped = new Map<string, TypeDefLine[]>()
  const classDefs = new Map<string, TypeDefLine>()

  for (const def of defs) {
    const namespace = def.js_mod ?? TOP_LEVEL_NAMESPACE
    if (!namespaceGrouped.has(namespace)) {
      namespaceGrouped.set(namespace, [])
    }

    const group = namespaceGrouped.get(namespace)!

    if (def.kind === TypeDefKind.Struct) {
      group.push(def)
      classDefs.set(def.name, def)
    } else if (def.kind === TypeDefKind.Extends) {
      const classDef = classDefs.get(def.name)
      if (classDef) {
        classDef.extends = def.def
      }
    } else if (def.kind === TypeDefKind.Impl) {
      // merge `impl` into class definition
      const classDef = classDefs.get(def.name)
      if (classDef) {
        if (classDef.def) {
          classDef.def += '\n'
        }

        classDef.def += def.def
        // Convert any remaining \n sequences in the merged def to actual newlines
        if (classDef.def) {
          classDef.def = classDef.def.replace(/\\n/g, '\n')
        }
      }
    } else {
      group.push(def)
    }
  }

  return namespaceGrouped
}

export function correctStringIdent(src: string, ident: number): string {
  let bracketDepth = 0
  const result = src
    .split('\n')
    .map((line) => {
      line = line.trim()
      if (line === '') {
        return ''
      }

      const isInMultilineComment = line.startsWith('*')
      const isClosingBracket = line.endsWith('}')
      const isOpeningBracket = line.endsWith('{')
      const isTypeDeclaration = line.endsWith('=')
      const isTypeVariant = line.startsWith('|')

      let rightIndent = ident
      if ((isOpeningBracket || isTypeDeclaration) && !isInMultilineComment) {
        bracketDepth += 1
        rightIndent += (bracketDepth - 1) * 2
      } else {
        if (
          isClosingBracket &&
          bracketDepth > 0 &&
          !isInMultilineComment &&
          !isTypeVariant
        ) {
          bracketDepth -= 1
        }
        rightIndent += bracketDepth * 2
      }

      if (isInMultilineComment) {
        rightIndent += 1
      }

      const s = `${' '.repeat(rightIndent)}${line}`

      return s
    })
    .join('\n')

  return result
}
