import type {
  CapabilityRequest,
  HostedContributionDefinition,
  HostedToolJsonSchema,
  HostedPostToolUseRequest,
  HostedPostToolUseResponse,
  HostedPreToolUseRequest,
  HostedPreToolUseResponse,
  HostedSystemPromptTransformRequest,
  HostedSystemPromptTransformResponse,
  HostedToolDiscoveryRequest,
  HostedToolDiscoveryResponse,
  HostedToolCapabilitiesRequest,
  HostedToolInvokeRequest,
  HostedToolContextSnapshot,
  ToolInputValue,
} from "@best-agent/protocol";
import { BestAgentError, isRecord } from "@best-agent/foundation";
import type { AgentProfile } from "../agent/profile-registry.js";
import type {
  ToolDefinition,
  ToolExecutionContext,
  ToolInputJsonSchema,
} from "../tool/registry.js";

export interface InstalledSkill {
  allowedTools?: string[];
  content: string;
  contextMode?: "append" | "replace";
  description: string;
  effort?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh";
  executionMode?: "inline" | "fork";
  id: string;
  model?: string;
  name: string;
  pathScope?: string[];
  source: "extension" | "plugin";
  sourceExtension?: string;
  sourcePlugin?: string;
  whenToUse?: string;
}

export interface RuntimeContributionBundle {
  agentProfiles: AgentProfile[];
  postToolUseHooks: InstalledPostToolUseHook[];
  preToolUseHooks: InstalledPreToolUseHook[];
  skills: InstalledSkill[];
  systemPromptHooks: InstalledSystemPromptHook[];
  toolDiscoveryHooks: InstalledToolDiscoveryHook[];
  tools: ToolDefinition[];
}

export interface InstalledSystemPromptHook {
  apply(
    request: Omit<HostedSystemPromptTransformRequest, "contributionName">,
  ): Promise<string | undefined>;
  contributionName: string;
}

export interface InstalledToolDiscoveryHook {
  apply(request: Omit<HostedToolDiscoveryRequest, "contributionName">): Promise<string[]>;
  contributionName: string;
}

export interface InstalledPreToolUseHook {
  apply(
    request: Omit<HostedPreToolUseRequest, "contributionName">,
  ): Promise<HostedPreToolUseResponse>;
  contributionName: string;
}

export interface InstalledPostToolUseHook {
  apply(
    request: Omit<HostedPostToolUseRequest, "contributionName">,
  ): Promise<HostedPostToolUseResponse>;
  contributionName: string;
}

export interface HostedToolBridge {
  postToolUse?: (request: HostedPostToolUseRequest) => Promise<HostedPostToolUseResponse>;
  preToolUse?: (request: HostedPreToolUseRequest) => Promise<HostedPreToolUseResponse>;
  transformSystemPrompt(
    request: HostedSystemPromptTransformRequest,
  ): Promise<HostedSystemPromptTransformResponse>;
  discoverTools(request: HostedToolDiscoveryRequest): Promise<HostedToolDiscoveryResponse>;
  getCapabilities(request: HostedToolCapabilitiesRequest): Promise<CapabilityRequest[]>;
  invoke(request: HostedToolInvokeRequest): Promise<string>;
}

const NO_INPUT_SCHEMA = {
  additionalProperties: false,
  properties: {},
  required: [],
  type: "object",
} satisfies ToolInputJsonSchema;

const HOSTED_TOOL_JSON_SCHEMA_KEYS = new Set([
  "additionalProperties",
  "enum",
  "items",
  "properties",
  "required",
  "type",
]);

const HOSTED_TOOL_JSON_SCHEMA_TYPES = new Set([
  "array",
  "boolean",
  "integer",
  "number",
  "object",
  "string",
]);

export function createHostedContributionBundle(
  contributions: HostedContributionDefinition[],
  bridge: HostedToolBridge,
): RuntimeContributionBundle {
  const agentProfiles: AgentProfile[] = [];
  const postToolUseHooks: InstalledPostToolUseHook[] = [];
  const preToolUseHooks: InstalledPreToolUseHook[] = [];
  const skills: InstalledSkill[] = [];
  const systemPromptHooks: InstalledSystemPromptHook[] = [];
  const toolDiscoveryHooks: InstalledToolDiscoveryHook[] = [];
  const tools: ToolDefinition[] = [];

  for (const contribution of contributions) {
    const localToolNames = new Map(
      (contribution.tools ?? []).map((tool) => [
        tool.name,
        qualifyHostedToolName(contribution.name, tool.name),
      ]),
    );
    const localSkillIds = new Map(
      (contribution.skills ?? []).map((skill) => [
        skill.name,
        qualifyHostedSkillId(contribution.name, skill.name),
      ]),
    );

    for (const tool of contribution.tools ?? []) {
      tools.push({
        approvalClass: tool.approvalClass,
        concurrencyGroup: tool.concurrencyGroup,
        description: tool.description,
        executionClass: tool.executionClass ?? "external-side-effect",
        expectedDurationClass: tool.expectedDurationClass,
        heartbeatPolicy: tool.heartbeatPolicy,
        idempotent: tool.idempotent ?? false,
        inputSchema: parseHostedToolInputSchema(contribution.name, tool.name, tool.inputSchema),
        name: qualifyHostedToolName(contribution.name, tool.name),
        replayPolicy: tool.replayPolicy,
        source: "plugin",
        sourcePlugin: contribution.name,
        capabilities: async (input, context) =>
          (
            await bridge.getCapabilities({
              context: toHostedToolContextSnapshot(context),
              contributionName: contribution.name,
              input,
              toolName: tool.name,
            })
          ).map((request) => ({
            ...request,
            kind: request.kind ?? "plugin-hook",
          })),
        execute: (input, context) =>
          bridge.invoke({
            context: toHostedToolContextSnapshot(context),
            contributionName: contribution.name,
            input,
            toolName: tool.name,
          }),
      });
    }

    for (const skill of contribution.skills ?? []) {
      skills.push({
        allowedTools: skill.allowedTools,
        content: skill.content,
        contextMode: skill.contextMode,
        description: skill.description,
        effort: skill.effort,
        executionMode: skill.executionMode,
        id: qualifyHostedSkillId(contribution.name, skill.name),
        model: skill.model,
        name: skill.name,
        pathScope: skill.pathScope,
        source: "plugin",
        sourcePlugin: contribution.name,
        whenToUse: skill.whenToUse,
      });
    }

    for (const profile of contribution.agentProfiles ?? []) {
      agentProfiles.push({
        allowedTools: normalizeToolRefs(profile.allowedTools ?? [], localToolNames),
        defaultSkills: normalizeSkillRefs(profile.defaultSkills ?? [], localSkillIds),
        description: profile.description,
        name: profile.name,
        source: "plugin",
        sourcePlugin: contribution.name,
        systemPrompt: profile.systemPrompt,
      });
    }

    if (contribution.hooks?.systemPromptTransform) {
      systemPromptHooks.push({
        apply: async (request) =>
          (
            await bridge.transformSystemPrompt({
              ...request,
              contributionName: contribution.name,
            })
          ).systemPrompt,
        contributionName: contribution.name,
      });
    }

    if (contribution.hooks?.toolDiscovery) {
      toolDiscoveryHooks.push({
        apply: async (request) =>
          (
            await bridge.discoverTools({
              ...request,
              contributionName: contribution.name,
            })
          ).toolNames,
        contributionName: contribution.name,
      });
    }

    if (contribution.hooks?.preToolUse) {
      preToolUseHooks.push({
        apply: async (request) =>
          (await bridge.preToolUse?.({
            ...request,
            contributionName: contribution.name,
          })) ?? {},
        contributionName: contribution.name,
      });
    }

    if (contribution.hooks?.postToolUse) {
      postToolUseHooks.push({
        apply: async (request) =>
          (await bridge.postToolUse?.({
            ...request,
            contributionName: contribution.name,
          })) ?? {},
        contributionName: contribution.name,
      });
    }
  }

  return {
    agentProfiles,
    postToolUseHooks,
    preToolUseHooks,
    skills,
    systemPromptHooks,
    toolDiscoveryHooks,
    tools,
  };
}

function parseHostedToolInputSchema(
  contributionName: string,
  toolName: string,
  inputSchema: HostedToolJsonSchema | undefined,
): ToolInputJsonSchema {
  if (inputSchema === undefined) {
    return {
      additionalProperties: NO_INPUT_SCHEMA.additionalProperties,
      properties: {},
      required: [],
      type: NO_INPUT_SCHEMA.type,
    };
  }
  const schema = parseHostedToolJsonSchema(
    inputSchema,
    `Hosted tool ${contributionName}:${toolName} inputSchema`,
  );
  if (schema.type !== "object") {
    throw new BestAgentError(
      "PLUGIN_VALIDATION",
      `Hosted tool ${contributionName}:${toolName} inputSchema root must set type: "object".`,
    );
  }
  return schema;
}

function parseHostedToolJsonSchema(value: unknown, path: string): ToolInputJsonSchema {
  if (!isRecord(value)) {
    throw new BestAgentError("PLUGIN_VALIDATION", `${path} must be a JSON schema object.`);
  }

  for (const key of Object.keys(value)) {
    if (!HOSTED_TOOL_JSON_SCHEMA_KEYS.has(key)) {
      throw new BestAgentError(
        "PLUGIN_VALIDATION",
        `${path}.${key} is not supported by hosted tool schemas.`,
      );
    }
  }

  const hasObjectKeywords =
    "additionalProperties" in value || "properties" in value || "required" in value;
  const hasArrayKeywords = "items" in value;
  if (hasObjectKeywords && value.type !== "object") {
    throw new BestAgentError(
      "PLUGIN_VALIDATION",
      `${path} uses object schema keywords and must set type: "object".`,
    );
  }
  if (hasArrayKeywords && value.type !== "array") {
    throw new BestAgentError("PLUGIN_VALIDATION", `${path} uses items and must set type: "array".`);
  }
  if (!("type" in value) && !("enum" in value)) {
    throw new BestAgentError("PLUGIN_VALIDATION", `${path} must set type or enum.`);
  }

  const schema: ToolInputJsonSchema = {};
  if ("type" in value) {
    schema.type = parseHostedToolJsonSchemaType(value.type, `${path}.type`);
  }
  if ("additionalProperties" in value) {
    if (typeof value.additionalProperties !== "boolean") {
      throw new BestAgentError(
        "PLUGIN_VALIDATION",
        `${path}.additionalProperties must be boolean.`,
      );
    }
    schema.additionalProperties = value.additionalProperties;
  }
  if (schema.type === "object" && schema.additionalProperties !== false) {
    throw new BestAgentError(
      "PLUGIN_VALIDATION",
      `${path} object schemas must set additionalProperties: false.`,
    );
  }
  if (schema.type === "array" && !("items" in value)) {
    throw new BestAgentError("PLUGIN_VALIDATION", `${path} array schemas must set items.`);
  }
  if ("required" in value) {
    if (
      !Array.isArray(value.required) ||
      !value.required.every((entry) => typeof entry === "string")
    ) {
      throw new BestAgentError("PLUGIN_VALIDATION", `${path}.required must be a string array.`);
    }
    schema.required = [...value.required];
  }
  if ("enum" in value) {
    if (!Array.isArray(value.enum) || !value.enum.every(isToolInputValue)) {
      throw new BestAgentError(
        "PLUGIN_VALIDATION",
        `${path}.enum must contain ToolInputValue entries.`,
      );
    }
    schema.enum = [...value.enum];
  }
  if ("items" in value) {
    schema.items = parseHostedToolJsonSchema(value.items, `${path}.items`);
  }
  if ("properties" in value) {
    if (!isRecord(value.properties)) {
      throw new BestAgentError("PLUGIN_VALIDATION", `${path}.properties must be an object.`);
    }
    schema.properties = Object.fromEntries(
      Object.entries(value.properties).map(([propertyName, propertySchema]) => [
        propertyName,
        parseHostedToolJsonSchema(propertySchema, `${path}.properties.${propertyName}`),
      ]),
    );
  }
  if (schema.required) {
    const propertyNames = new Set(Object.keys(schema.properties ?? {}));
    for (const requiredName of schema.required) {
      if (!propertyNames.has(requiredName)) {
        throw new BestAgentError(
          "PLUGIN_VALIDATION",
          `${path}.required contains "${requiredName}" but properties.${requiredName} is not defined.`,
        );
      }
    }
  }
  return schema;
}

function parseHostedToolJsonSchemaType(
  value: unknown,
  path: string,
): NonNullable<ToolInputJsonSchema["type"]> {
  if (typeof value === "string" && HOSTED_TOOL_JSON_SCHEMA_TYPES.has(value)) {
    return value as NonNullable<ToolInputJsonSchema["type"]>;
  }
  throw new BestAgentError("PLUGIN_VALIDATION", `${path} must be a supported JSON schema type.`);
}

function isToolInputValue(value: unknown): value is ToolInputValue {
  if (value === null) {
    return true;
  }
  if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
    return true;
  }
  if (Array.isArray(value)) {
    return value.every(isToolInputValue);
  }
  if (isRecord(value)) {
    return Object.values(value).every(isToolInputValue);
  }
  return false;
}

export function qualifyHostedToolName(contributionName: string, toolName: string): string {
  return `plugin:${contributionName}:${toolName}`;
}

export function qualifyHostedSkillId(contributionName: string, skillName: string): string {
  return `skill:${contributionName}:${skillName}`;
}

function normalizeToolRefs(refs: string[], localToolNames: Map<string, string>): string[] {
  return Array.from(new Set(refs.map((ref) => localToolNames.get(ref) ?? ref)));
}

function normalizeSkillRefs(refs: string[], localSkillIds: Map<string, string>): string[] {
  return Array.from(new Set(refs.map((ref) => localSkillIds.get(ref) ?? ref)));
}

function toHostedToolContextSnapshot(context: ToolExecutionContext): HostedToolContextSnapshot {
  return {
    cwd: context.cwd,
    threadId: context.threadId,
    threadPaths: context.threadPaths,
    turnId: context.turnId,
  };
}
