import type { AdminPage, AdminParts, EditorPart, PublicPart, RegisteredAddon } from '../types/addon'

interface AddonModule {
  register( cb: ( addon: RegisteredAddon ) => void ): void
}

interface AddonPayload {
  name: string
  url: string
}

// Injectable importer keeps the dynamic import testable; defaults to native ESM import.
// Addons build ESM (dist/main.js) and `export const register = …`, so we import the
// module and call its exported register() — no classic <script> injection, no reading a
// mutated global path (both of which silently failed for ESM addons).
const importModule = ( url: string ): Promise<Partial<AddonModule>> =>
  ( ( globalThis as any ).__mockImport ?? ( ( u: string ) => import( /* @vite-ignore */ u ) ) )( url )

export class PluginsLoader {
  private plugins = new Map<string, RegisteredAddon['component']>()
  private editor = new Map<string, EditorPart>()
  private admin: AdminParts[] = []
  private adminPagesCache: AdminPage[] | null = null
  private errors = new Map<string, Error>()
  private loading: Promise<void> | null = null

  // Every form on a page calls load(); they all share one pass so each add-on is
  // imported once. Concurrent import() of a module still in its top-level await
  // resolves early in Safari, before the module's exports are initialised.
  load(): Promise<void> {
    this.loading ??= this.loadAll()
    return this.loading
  }

  private async loadAll(): Promise<void> {
    const payload = ( ( window as any ).__EPICFORMBUILDER__?.__PLUGINS__ ?? {} ) as Record<string, AddonPayload>
    await Promise.all( Object.values( payload ).map( ( cfg ) => this.loadOne( cfg ) ) )
  }

  private async loadOne( cfg: AddonPayload ): Promise<void> {
    try {
      const mod = await importModule( cfg.url )
      if ( typeof mod.register !== 'function' ) {
        throw new Error( `addon '${ cfg.name }' has no exported register()` )
      }
      mod.register( ( addon ) => {
        this.plugins.set( addon.name.toLowerCase(), addon.component )
        for ( const [ component, part ] of Object.entries( addon.editor ?? {} ) ) {
          this.editor.set( component, part )
        }
        if ( addon.admin ) { this.admin.push( addon.admin ); this.adminPagesCache = null }
      } )
    } catch ( err ) {
      const e = err instanceof Error ? err : new Error( String( err ) )
      this.errors.set( cfg.name.toLowerCase(), e )
      // Surface the failure — never swallow it into a "not installed" paywall.
      console.error( `EpicForms: addon '${ cfg.name }' failed to load from ${ cfg.url }`, e )
    }
  }

  getPlugin( name: string ): unknown | null {
    return this.plugins.get( `epicforms-${ name.toLowerCase() }` ) ?? null
  }

  pluginExists( name: string ): boolean {
    return this.plugins.has( `epicforms-${ name.toLowerCase() }` )
  }

  getEditorPart( component: string ): EditorPart | null {
    return this.editor.get( component ) ?? null
  }

  editorParts(): Array<[ string, EditorPart ]> {
    return [ ...this.editor.entries() ]
  }

  publicParts(): Array<[ string, PublicPart ]> {
    return [ ...this.plugins.entries() ] as Array<[ string, PublicPart ]>
  }

  adminParts(): AdminParts[] {
    return [ ...this.admin ]
  }

  adminPages(): AdminPage[] {
    if ( this.adminPagesCache ) return this.adminPagesCache
    const seen = new Map<string, AdminPage>()
    for ( const part of this.admin ) for ( const page of part.pages ?? [] ) {
      if ( seen.has( page.path ) ) { console.warn( `EpicForms: add-on page "${ page.path }" is registered twice; keeping "${ seen.get( page.path )!.title }", ignoring "${ page.title }"` ); continue }
      seen.set( page.path, page )
    }
    this.adminPagesCache = [ ...seen.values() ]
    return this.adminPagesCache
  }

  adminPage( path: string ): AdminPage | null {
    return this.adminPages().find( p => p.path === path ) ?? null
  }

  /**
   * Non-null when a *purchased* addon failed to load (distinct from "not installed").
   * A bundle that registers several add-ons (one name in the payload, many fields)
   * can't say which fields it would have provided, so any failure counts for a
   * field that isn't registered.
   */
  loadError( name: string ): Error | null {
    return this.errors.get( `epicforms-${ name.toLowerCase() }` ) ?? this.errors.values().next().value ?? null
  }
}

export default new PluginsLoader()
