import fs from "node:fs";
import path from "node:path";
import AjvModule from "ajv";
const Ajv = (AjvModule as any).default || AjvModule;
import { pluginManifestSchema } from "./schema.js";
import type { HookName, PluginManifest, ResolvedHookContribution } from "./types.js";

export interface PluginRecord {
  manifest: PluginManifest;
  path: string;
  enabled: boolean;
  valid: boolean;
  errors?: string[];
}

export class PluginRegistry {
  private ajv = new Ajv({ allErrors: true });
  private validate = this.ajv.compile(pluginManifestSchema);
  private plugins = new Map<string, PluginRecord>();
  private state = new Map<string, boolean>();

  constructor(
    private pluginsDir: string,
    private stateFile: string,
    private basePath = "",
  ) {
    this.loadState();
  }

  private loadState() {
    this.state.clear();
    if (!fs.existsSync(this.stateFile)) return;
    try {
      const raw = JSON.parse(fs.readFileSync(this.stateFile, "utf8")) as { enabled?: Record<string, boolean> };
      for (const [id, enabled] of Object.entries(raw.enabled || {})) {
        this.state.set(id, !!enabled);
      }
    } catch {
      // ignore corrupt state file and fall back to defaults
    }
  }

  private saveState() {
    fs.mkdirSync(path.dirname(this.stateFile), { recursive: true });
    const enabled = Object.fromEntries([...this.state.entries()]);
    fs.writeFileSync(this.stateFile, JSON.stringify({ enabled }, null, 2));
  }

  load(): PluginRecord[] {
    this.plugins.clear();
    this.loadState();
    if (!fs.existsSync(this.pluginsDir)) return [];
    const dirs = fs
      .readdirSync(this.pluginsDir, { withFileTypes: true })
      .filter((d) => d.isDirectory())
      .map((d) => d.name);
    for (const dir of dirs) {
      const pluginPath = path.join(this.pluginsDir, dir);
      const manifestPath = path.join(pluginPath, "plugin.json");
      if (!fs.existsSync(manifestPath)) continue;
      try {
        const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")) as PluginManifest;
        const valid = this.validate(manifest);
        const enabled = this.state.has(manifest.id) ? !!this.state.get(manifest.id) : !!manifest.enabledByDefault;
        this.plugins.set(manifest.id, {
          manifest,
          path: pluginPath,
          enabled,
          valid: !!valid,
          errors: valid ? [] : (this.validate.errors || []).map((e: any) => `${e.instancePath || "/"} ${e.message}`),
        });
      } catch (err: any) {
        this.plugins.set(dir, {
          manifest: { id: dir, name: dir, version: "0.0.0" },
          path: pluginPath,
          enabled: false,
          valid: false,
          errors: [String(err?.message || err)],
        });
      }
    }
    return [...this.plugins.values()];
  }

  list() {
    return [...this.plugins.values()];
  }

  get(id: string) {
    return this.plugins.get(id);
  }

  enable(id: string) {
    const r = this.plugins.get(id);
    if (r) {
      r.enabled = true;
      this.state.set(id, true);
      this.saveState();
    }
    return r;
  }

  disable(id: string) {
    const r = this.plugins.get(id);
    if (r) {
      r.enabled = false;
      this.state.set(id, false);
      this.saveState();
    }
    return r;
  }

  private resolveContribution(pluginId: string, manifest: PluginManifest, contribution: any): ResolvedHookContribution {
    const uiBase = `${this.basePath}/plugins/${pluginId}/ui`;
    const apiBase = `${this.basePath}/plugins/${pluginId}/api`;
    return {
      ...contribution,
      pluginId,
      pluginName: manifest.name,
      pluginVersion: manifest.version,
      uiBase,
      apiBase,
      mountUrl: contribution.mount ? `${uiBase}/${contribution.mount.replace(/^\/+/, "")}` : undefined,
      cssUrl: contribution.css ? `${uiBase}/${contribution.css.replace(/^\/+/, "")}` : undefined,
    };
  }

  hooksIndex() {
    const m: Record<string, string[]> = {};
    for (const r of this.plugins.values()) {
      if (!r.enabled || !r.valid) continue;
      for (const h of r.manifest.hooks || []) {
        m[h] ||= [];
        m[h].push(r.manifest.id);
      }
    }
    return m;
  }

  resolvedHooks() {
    const m: Record<string, ResolvedHookContribution[]> = {};
    for (const r of this.plugins.values()) {
      if (!r.enabled || !r.valid) continue;
      const contributions = r.manifest.contributions || {};
      for (const [hook, items] of Object.entries(contributions) as [HookName, any[]][]) {
        if (!items?.length) continue;
        m[hook] ||= [];
        for (const item of items) {
          m[hook].push(this.resolveContribution(r.manifest.id, r.manifest, item));
        }
        m[hook].sort((a, b) => (a.order || 0) - (b.order || 0));
      }
    }
    return m;
  }
}
