/**
 * PluginCapabilityValidator — enforces the capability model at both
 * install-time and runtime.
 *
 * Every plugin declares the capabilities it requires in its manifest
 * (`manifest.capabilities`). This service checks those declarations
 * against a mapping of operations → required capabilities so that:
 *
 * 1. **Install-time validation** — `validateManifestCapabilities()`
 *    ensures that declared features (tools, jobs, webhooks, UI slots)
 *    have matching capability entries, giving operators clear feedback
 *    before a plugin is activated.
 *
 * 2. **Runtime gating** — `checkOperation()` / `assertOperation()` are
 *    called on every worker→host bridge call to enforce least-privilege
 *    access. If a plugin attempts an operation it did not declare, the
 *    call is rejected with a 403 error.
 *
 * @see PLUGIN_SPEC.md §15 — Capability Model
 * @see host-client-factory.ts — SDK-side capability gating
 */
import type {
  PluginCapability,
  PaperclipPluginManifestV1,
  PluginUiSlotType,
  PluginLauncherPlacementZone,
} from "@paperclipai/shared";
import { forbidden } from "../errors.js";
import { logger } from "../middleware/logger.js";

// ---------------------------------------------------------------------------
// Capability requirement mappings
// ---------------------------------------------------------------------------

/**
 * Maps high-level operations to the capabilities they require.
 *
 * When the bridge receives a call from a plugin worker, the host looks up
 * the operation in this map and checks the plugin's declared capabilities.
 * If any required capability is missing, the call is rejected.
 *
 * @see PLUGIN_SPEC.md §15 — Capability Model
 */
const OPERATION_CAPABILITIES: Record<string, readonly PluginCapability[]> = {
  // Data read operations
  "companies.list": ["companies.read"],
  "companies.get": ["companies.read"],
  "projects.list": ["projects.read"],
  "projects.get": ["projects.read"],
  "project.workspaces.list": ["project.workspaces.read"],
  "project.workspaces.get": ["project.workspaces.read"],
  "issues.list": ["issues.read"],
  "issues.get": ["issues.read"],
  "issue.comments.list": ["issue.comments.read"],
  "issue.comments.get": ["issue.comments.read"],
  "agents.list": ["agents.read"],
  "agents.get": ["agents.read"],
  "goals.list": ["goals.read"],
  "goals.get": ["goals.read"],
  "activity.list": ["activity.read"],
  "activity.get": ["activity.read"],
  "costs.list": ["costs.read"],
  "costs.get": ["costs.read"],

  // Data write operations
  "issues.create": ["issues.create"],
  "issues.update": ["issues.update"],
  "issue.comments.create": ["issue.comments.create"],
  "activity.log": ["activity.log.write"],
  "metrics.write": ["metrics.write"],
  "telemetry.track": ["telemetry.track"],

  // Plugin state operations
  "plugin.state.get": ["plugin.state.read"],
  "plugin.state.list": ["plugin.state.read"],
  "plugin.state.set": ["plugin.state.write"],
  "plugin.state.delete": ["plugin.state.write"],

  // Runtime / Integration operations
  "events.subscribe": ["events.subscribe"],
  "events.emit": ["events.emit"],
  "jobs.schedule": ["jobs.schedule"],
  "jobs.cancel": ["jobs.schedule"],
  "webhooks.receive": ["webhooks.receive"],
  "http.request": ["http.outbound"],
  "secrets.resolve": ["secrets.read-ref"],

  // Agent tools
  "agent.tools.register": ["agent.tools.register"],
  "agent.tools.execute": ["agent.tools.register"],
};

/**
 * Maps UI slot types to the capability required to register them.
 *
 * @see PLUGIN_SPEC.md §19 — UI Extension Model
 */
const UI_SLOT_CAPABILITIES: Record<PluginUiSlotType, PluginCapability> = {
  sidebar: "ui.sidebar.register",
  sidebarPanel: "ui.sidebar.register",
  projectSidebarItem: "ui.sidebar.register",
  page: "ui.page.register",
  detailTab: "ui.detailTab.register",
  taskDetailView: "ui.detailTab.register",
  dashboardWidget: "ui.dashboardWidget.register",
  globalToolbarButton: "ui.action.register",
  toolbarButton: "ui.action.register",
  contextMenuItem: "ui.action.register",
  commentAnnotation: "ui.commentAnnotation.register",
  commentContextMenuItem: "ui.action.register",
  settingsPage: "instance.settings.register",
};

/**
 * Launcher placement zones align with host UI surfaces and therefore inherit
 * the same capability requirements as the equivalent slot type.
 */
const LAUNCHER_PLACEMENT_CAPABILITIES: Record<
  PluginLauncherPlacementZone,
  PluginCapability
> = {
  page: "ui.page.register",
  detailTab: "ui.detailTab.register",
  taskDetailView: "ui.detailTab.register",
  dashboardWidget: "ui.dashboardWidget.register",
  sidebar: "ui.sidebar.register",
  sidebarPanel: "ui.sidebar.register",
  projectSidebarItem: "ui.sidebar.register",
  globalToolbarButton: "ui.action.register",
  toolbarButton: "ui.action.register",
  contextMenuItem: "ui.action.register",
  commentAnnotation: "ui.commentAnnotation.register",
  commentContextMenuItem: "ui.action.register",
  settingsPage: "instance.settings.register",
};

/**
 * Maps feature declarations in the manifest to their required capabilities.
 */
const FEATURE_CAPABILITIES: Record<string, PluginCapability> = {
  tools: "agent.tools.register",
  jobs: "jobs.schedule",
  webhooks: "webhooks.receive",
};

// ---------------------------------------------------------------------------
// Result types
// ---------------------------------------------------------------------------

/**
 * Result of a capability check. When `allowed` is false, `missing` contains
 * the capabilities that the plugin does not declare but the operation requires.
 */
export interface CapabilityCheckResult {
  allowed: boolean;
  missing: PluginCapability[];
  operation?: string;
  pluginId?: string;
}

// ---------------------------------------------------------------------------
// PluginCapabilityValidator interface
// ---------------------------------------------------------------------------

export interface PluginCapabilityValidator {
  /**
   * Check whether a plugin has a specific capability.
   */
  hasCapability(
    manifest: PaperclipPluginManifestV1,
    capability: PluginCapability,
  ): boolean;

  /**
   * Check whether a plugin has all of the specified capabilities.
   */
  hasAllCapabilities(
    manifest: PaperclipPluginManifestV1,
    capabilities: PluginCapability[],
  ): CapabilityCheckResult;

  /**
   * Check whether a plugin has at least one of the specified capabilities.
   */
  hasAnyCapability(
    manifest: PaperclipPluginManifestV1,
    capabilities: PluginCapability[],
  ): boolean;

  /**
   * Check whether a plugin is allowed to perform the named operation.
   *
   * Operations are mapped to required capabilities via OPERATION_CAPABILITIES.
   * Unknown operations are rejected by default.
   */
  checkOperation(
    manifest: PaperclipPluginManifestV1,
    operation: string,
  ): CapabilityCheckResult;

  /**
   * Assert that a plugin is allowed to perform an operation.
   * Throws a 403 HttpError if the capability check fails.
   */
  assertOperation(
    manifest: PaperclipPluginManifestV1,
    operation: string,
  ): void;

  /**
   * Assert that a plugin has a specific capability.
   * Throws a 403 HttpError if the capability is missing.
   */
  assertCapability(
    manifest: PaperclipPluginManifestV1,
    capability: PluginCapability,
  ): void;

  /**
   * Check whether a plugin can register the given UI slot type.
   */
  checkUiSlot(
    manifest: PaperclipPluginManifestV1,
    slotType: PluginUiSlotType,
  ): CapabilityCheckResult;

  /**
   * Validate that a manifest's declared capabilities are consistent with its
   * declared features (tools, jobs, webhooks, UI slots).
   *
   * Returns all missing capabilities rather than failing on the first one.
   * This is useful for install-time validation to give comprehensive feedback.
   */
  validateManifestCapabilities(
    manifest: PaperclipPluginManifestV1,
  ): CapabilityCheckResult;

  /**
   * Get the capabilities required for a named operation.
   * Returns an empty array if the operation is unknown.
   */
  getRequiredCapabilities(operation: string): readonly PluginCapability[];

  /**
   * Get the capability required for a UI slot type.
   */
  getUiSlotCapability(slotType: PluginUiSlotType): PluginCapability;
}

// ---------------------------------------------------------------------------
// Factory
// ---------------------------------------------------------------------------

/**
 * Create a PluginCapabilityValidator.
 *
 * This service enforces capability gates for plugin operations.  The host
 * uses it to verify that a plugin's declared capabilities permit the
 * operation it is attempting, both at install time (manifest validation)
 * and at runtime (bridge call gating).
 *
 * Usage:
 * ```ts
 * const validator = pluginCapabilityValidator();
 *
 * // Runtime: gate a bridge call
 * validator.assertOperation(plugin.manifestJson, "issues.create");
 *
 * // Install time: validate manifest consistency
 * const result = validator.validateManifestCapabilities(manifest);
 * if (!result.allowed) {
 *   throw badRequest("Missing capabilities", result.missing);
 * }
 * ```
 */
export function pluginCapabilityValidator(): PluginCapabilityValidator {
  const log = logger.child({ service: "plugin-capability-validator" });

  // -----------------------------------------------------------------------
  // Internal helpers
  // -----------------------------------------------------------------------

  function capabilitySet(manifest: PaperclipPluginManifestV1): Set<PluginCapability> {
    return new Set(manifest.capabilities);
  }

  function buildForbiddenMessage(
    manifest: PaperclipPluginManifestV1,
    operation: string,
    missing: PluginCapability[],
  ): string {
    return (
      `Plugin '${manifest.id}' is not allowed to perform '${operation}'. ` +
      `Missing required capabilities: ${missing.join(", ")}`
    );
  }

  // -----------------------------------------------------------------------
  // Public API
  // -----------------------------------------------------------------------

  return {
    hasCapability(manifest, capability) {
      return manifest.capabilities.includes(capability);
    },

    hasAllCapabilities(manifest, capabilities) {
      const declared = capabilitySet(manifest);
      const missing = capabilities.filter((cap) => !declared.has(cap));
      return {
        allowed: missing.length === 0,
        missing,
        pluginId: manifest.id,
      };
    },

    hasAnyCapability(manifest, capabilities) {
      const declared = capabilitySet(manifest);
      return capabilities.some((cap) => declared.has(cap));
    },

    checkOperation(manifest, operation) {
      const required = OPERATION_CAPABILITIES[operation];

      if (!required) {
        log.warn(
          { pluginId: manifest.id, operation },
          "capability check for unknown operation – rejecting by default",
        );
        return {
          allowed: false,
          missing: [],
          operation,
          pluginId: manifest.id,
        };
      }

      const declared = capabilitySet(manifest);
      const missing = required.filter((cap) => !declared.has(cap));

      if (missing.length > 0) {
        log.debug(
          { pluginId: manifest.id, operation, missing },
          "capability check failed",
        );
      }

      return {
        allowed: missing.length === 0,
        missing,
        operation,
        pluginId: manifest.id,
      };
    },

    assertOperation(manifest, operation) {
      const result = this.checkOperation(manifest, operation);
      if (!result.allowed) {
        const msg = result.missing.length > 0
          ? buildForbiddenMessage(manifest, operation, result.missing)
          : `Plugin '${manifest.id}' attempted unknown operation '${operation}'`;
        throw forbidden(msg);
      }
    },

    assertCapability(manifest, capability) {
      if (!this.hasCapability(manifest, capability)) {
        throw forbidden(
          `Plugin '${manifest.id}' lacks required capability '${capability}'`,
        );
      }
    },

    checkUiSlot(manifest, slotType) {
      const required = UI_SLOT_CAPABILITIES[slotType];
      if (!required) {
        return {
          allowed: false,
          missing: [],
          operation: `ui.${slotType}.register`,
          pluginId: manifest.id,
        };
      }

      const has = manifest.capabilities.includes(required);
      return {
        allowed: has,
        missing: has ? [] : [required],
        operation: `ui.${slotType}.register`,
        pluginId: manifest.id,
      };
    },

    validateManifestCapabilities(manifest) {
      const declared = capabilitySet(manifest);
      const allMissing: PluginCapability[] = [];

      // Check feature declarations → required capabilities
      for (const [feature, requiredCap] of Object.entries(FEATURE_CAPABILITIES)) {
        const featureValue = manifest[feature as keyof PaperclipPluginManifestV1];
        if (Array.isArray(featureValue) && featureValue.length > 0) {
          if (!declared.has(requiredCap)) {
            allMissing.push(requiredCap);
          }
        }
      }

      // Check UI slots → required capabilities
      const uiSlots = manifest.ui?.slots ?? [];
      if (uiSlots.length > 0) {
        for (const slot of uiSlots) {
          const requiredCap = UI_SLOT_CAPABILITIES[slot.type];
          if (requiredCap && !declared.has(requiredCap)) {
            if (!allMissing.includes(requiredCap)) {
              allMissing.push(requiredCap);
            }
          }
        }
      }

      // Check launcher declarations → required capabilities
      const launchers = [
        ...(manifest.launchers ?? []),
        ...(manifest.ui?.launchers ?? []),
      ];
      if (launchers.length > 0) {
        for (const launcher of launchers) {
          const requiredCap = LAUNCHER_PLACEMENT_CAPABILITIES[launcher.placementZone];
          if (requiredCap && !declared.has(requiredCap) && !allMissing.includes(requiredCap)) {
            allMissing.push(requiredCap);
          }
        }
      }

      return {
        allowed: allMissing.length === 0,
        missing: allMissing,
        pluginId: manifest.id,
      };
    },

    getRequiredCapabilities(operation) {
      return OPERATION_CAPABILITIES[operation] ?? [];
    },

    getUiSlotCapability(slotType) {
      return UI_SLOT_CAPABILITIES[slotType];
    },
  };
}
