import type { MarkSpec, NodeSpec, Schema, SchemaSpec } from '@tiptap/pm/model'

import type { getSchemaSpecByResolvedExtensions } from '../helpers/getSchemaSpecByResolvedExtensions.js'
import { markSchemaFields, nodeSchemaFields } from './schemaSpecFields.js'

type LocalSchemaSpec = ReturnType<typeof getSchemaSpecByResolvedExtensions>
type SchemaFields = Record<string, 'string' | 'boolean' | readonly string[]>

function structuralFields(spec: NodeSpec | MarkSpec, fields: SchemaFields, path: string) {
  return Object.fromEntries(
    Object.entries(fields).flatMap(([name, expected]) => {
      const value: unknown = spec[name]

      if (value === undefined) {
        return []
      }

      const valid =
        typeof expected === 'string'
          ? typeof value === expected
          : typeof value === 'string' && expected.includes(value)

      if (!valid) {
        throw new RangeError(`Invalid schemaSpec field '${path}.${name}'`)
      }

      return [[name, value]]
    }),
  )
}

function mergeType(
  name: string,
  local: NodeSpec | MarkSpec,
  external: NodeSpec | MarkSpec,
  fields: SchemaFields,
) {
  const behavior = Object.fromEntries(
    Object.entries(local).filter(([key]) => key !== 'attrs' && !Object.hasOwn(fields, key)),
  )

  return {
    ...behavior,
    ...structuralFields(external, fields, name),
    attrs: external.attrs,
  }
}

function checkTypes(local: Record<string, object>, external: Record<string, object>, kind: string) {
  for (const name of Object.keys(external)) {
    if (!Object.hasOwn(local, name)) {
      throw new RangeError(`schemaSpec requires a local ${kind} extension named '${name}'`)
    }
  }

  for (const name of Object.keys(local)) {
    if (!Object.hasOwn(external, name)) {
      throw new RangeError(`Local ${kind} extension '${name}' is not in schemaSpec`)
    }
  }
}

/**
 * Combines external structure with local DOM behavior and extension metadata.
 * @param local The specifications generated by resolved local extensions
 * @param external The schema compiled from the provided specification
 * @returns A specification in the external schema's node and mark order
 */
export function mergeSchemaSpec(local: LocalSchemaSpec, external: Schema): SchemaSpec {
  checkTypes(local.nodes, external.nodes, 'node')
  checkTypes(local.marks, external.marks, 'mark')
  let nodes = external.spec.nodes
  let marks = external.spec.marks

  external.spec.nodes.forEach((name, spec) => {
    nodes = nodes.update(name, mergeType(name, local.nodes[name], spec, nodeSchemaFields))
  })
  external.spec.marks.forEach((name, spec) => {
    marks = marks.update(name, mergeType(name, local.marks[name], spec, markSchemaFields))
  })

  return { topNode: external.topNodeType.name, nodes, marks }
}
