import { mkdir, writeFile } from "node:fs/promises";
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
import { dirname } from "node:path";

export type JsonSchema = Record<string, unknown>;
export type AgentDslLanguage = "typescript" | "javascript";
export type AuthStrategy = "public" | "api_key" | "platform_user" | "jwt" | "custom";

export type AgentIdentity = {
  name: string;
  description: string;
  version?: string;
};

export type AgentDslAuth = {
  model: string;
  strategy: AuthStrategy;
  principal_schema: JsonSchema;
  resolver?: string | null;
  required: boolean;
};

export type SkillPolicy = {
  timeout_seconds?: number | null;
  idempotent?: boolean;
  max_retries?: number;
  cost_class?: string | null;
  allow_scope_expansion?: boolean;
  grant_mode?: string | null;
  grant_allow_patterns?: string[];
  grant_deny_patterns?: string[];
  grant_outputs_prefix?: string | null;
  grant_write_prefixes?: string[];
  grant_ttl_seconds?: number | null;
  grant_run_timeout_seconds?: number | null;
  grant_approval_timeout_seconds?: number | null;
  grant_scope_approval_timeout_seconds?: number | null;
};

export type SkillDeclaration = {
  name: string;
  description?: string;
  handler?: string;
  tags?: string[];
  scopes?: string[];
  stream?: boolean;
  policy?: SkillPolicy;
  input_schema: JsonSchema;
  output_schema: JsonSchema;
};

export type AgentRuntime = {
  lifecycle?: "ephemeral" | "session" | "warm";
  state?: "none" | "session" | "durable";
  sandbox?: "microsandbox";
  resources?: {
    cpu?: string;
    memory?: string;
    gpu?: number;
    max_runtime_seconds?: number;
  };
  concurrency?: number;
  egress?: {
    allow_hosts?: string[];
    allow_internal_services?: string[];
    deny_internet_by_default?: boolean;
  };
  tools_used?: string[];
  llm_provisioning?: "platform" | "platform_or_caller_provided" | "caller_provided" | "agent_byok";
  pricing?: {
    price_per_call_usd?: number;
    caller_pays_llm?: boolean;
    notes?: string;
  };
  wants_cp_jwt?: boolean;
};

export type AgentDslEntrypoint = {
  module?: string | null;
  class_name?: string | null;
  function?: string | null;
  command?: string[];
};

export type AgentDslSkill = Required<Pick<SkillDeclaration, "name" | "input_schema" | "output_schema">> & {
  description: string;
  handler: string;
  tags: string[];
  scopes: string[];
  stream: boolean;
  policy: Required<SkillPolicy>;
};

export type AgentDsl = {
  schema_version: "2026-06-04";
  language: AgentDslLanguage;
  name: string;
  description: string;
  version: string;
  entrypoint: AgentDslEntrypoint;
  skills: AgentDslSkill[];
  capabilities: Record<string, unknown>;
  input_modes: string[];
  output_modes: string[];
  required_secrets: string[];
  required_env: string[];
  consumer_setup: { fields: unknown[] };
  runtime: AgentRuntime;
  template_lineage?: unknown | null;
  meta_agent_manifest?: unknown | null;
  state_schema?: JsonSchema | null;
  workspace_access: Record<string, unknown>;
  config_schema?: JsonSchema | null;
  auth: AgentDslAuth;
  metadata: Record<string, unknown>;
};

export type AgentClass<T extends A2AAgent = A2AAgent> = {
  new (): T;
  agent: AgentIdentity;
  auth: AgentDslAuth;
  skills: SkillDeclaration[];
  capabilities: Record<string, unknown>;
  inputModes: string[];
  outputModes: string[];
  requiredSecrets: string[];
  requiredEnv: string[];
  consumerSetup: { fields: unknown[] };
  runtime: AgentRuntime;
  workspaceAccess: Record<string, unknown>;
  configSchema: JsonSchema | null;
  stateSchema: JsonSchema | null;
  templateLineage: unknown | null;
  metaAgentManifest: unknown | null;
};

export abstract class A2AAgent {
  static agent: AgentIdentity;
  static auth: AgentDslAuth = publicAuth();
  static skills: SkillDeclaration[] = [];
  static capabilities: Record<string, unknown> = {};
  static inputModes: string[] = ["application/json"];
  static outputModes: string[] = ["application/json"];
  static requiredSecrets: string[] = [];
  static requiredEnv: string[] = [];
  static consumerSetup: { fields: unknown[] } = { fields: [] };
  static runtime: AgentRuntime = {};
  static workspaceAccess: Record<string, unknown> = { enabled: false };
  static configSchema: JsonSchema | null = null;
  static stateSchema: JsonSchema | null = null;
  static templateLineage: unknown | null = null;
  static metaAgentManifest: unknown | null = null;

  async dispatch(request: WorkerRequest): Promise<WorkerResponse> {
    const handler = (this as Record<string, unknown>)[request.handler];
    if (typeof handler !== "function") {
      throw new Error(`agent does not implement handler method: ${request.handler}`);
    }
    return normalizeWorkerResponse(await handler.call(this, request));
  }
}

export type WorkerRequest = {
  agent: string;
  skill: string;
  handler: string;
  arguments: Record<string, unknown>;
  grant?: string | null;
  llm_creds?: Record<string, unknown> | null;
  composition?: Record<string, unknown> | null;
  consumer_config?: Record<string, unknown> | null;
  consumer_secrets?: Record<string, string> | null;
  cp_jwt?: string | null;
  cp_url?: string | null;
  auth?: Record<string, unknown> | null;
};

export type WorkerResponse = {
  result: unknown;
  events?: Record<string, unknown>[];
  artifacts?: Record<string, unknown>[];
};

export type Handler = (request: WorkerRequest) => WorkerResponse | Promise<WorkerResponse>;

export function publicAuth(): AgentDslAuth {
  return {
    model: "NoAuth",
    strategy: "public",
    principal_schema: { title: "NoAuth", type: "object", properties: {}, additionalProperties: false },
    required: false
  };
}

export function skill(declaration: SkillDeclaration): SkillDeclaration {
  validateSkill(declaration);
  return declaration;
}

export function compileAgent(
  agentClass: AgentClass,
  options: {
    language?: AgentDslLanguage;
    entrypoint?: AgentDslEntrypoint;
    metadata?: Record<string, unknown>;
  } = {}
): AgentDsl {
  const identity = agentClass.agent;
  if (!identity?.name) throw new Error("agent.agent.name is required");
  const skills = agentClass.skills.map(normalizeSkill);
  if (skills.length === 0) throw new Error("agent must declare at least one skill");
  const names = new Set<string>();
  for (const item of skills) {
    if (names.has(item.name)) throw new Error(`duplicate skill: ${item.name}`);
    names.add(item.name);
  }
  return {
    schema_version: "2026-06-04",
    language: options.language ?? "typescript",
    name: identity.name,
    description: identity.description ?? "",
    version: identity.version ?? "0.1.0",
    entrypoint: options.entrypoint ?? {},
    skills,
    capabilities: agentClass.capabilities,
    input_modes: agentClass.inputModes,
    output_modes: agentClass.outputModes,
    required_secrets: agentClass.requiredSecrets,
    required_env: agentClass.requiredEnv,
    consumer_setup: agentClass.consumerSetup,
    runtime: { sandbox: "microsandbox", ...agentClass.runtime },
    template_lineage: agentClass.templateLineage,
    meta_agent_manifest: agentClass.metaAgentManifest,
    state_schema: agentClass.stateSchema,
    workspace_access: agentClass.workspaceAccess,
    config_schema: agentClass.configSchema,
    auth: agentClass.auth,
    metadata: options.metadata ?? {}
  };
}

export async function writeAgentDsl(
  agentClass: AgentClass,
  path = ".a2a/agent.dsl.json",
  options: Parameters<typeof compileAgent>[1] = {}
): Promise<AgentDsl> {
  const dsl = compileAgent(agentClass, options);
  await mkdir(dirname(path), { recursive: true });
  await writeFile(path, `${JSON.stringify(dsl, null, 2)}\n`, "utf8");
  return dsl;
}

export function serveWorker(
  agentClass: AgentClass,
  handlers: Record<string, Handler>,
  options: { port?: number; host?: string } = {}
): Server {
  const dsl = compileAgent(agentClass);
  const handlerNames = new Set(dsl.skills.map((item) => item.handler));
  const server = createServer(async (req, res) => {
    try {
      if (req.method !== "POST" || !req.url?.startsWith("/_a2a/invoke/")) {
        writeJson(res, 404, { error: "not found" });
        return;
      }
      const handlerName = decodeURIComponent(req.url.slice("/_a2a/invoke/".length));
      if (!handlerNames.has(handlerName) || typeof handlers[handlerName] !== "function") {
        writeJson(res, 404, { error: `unknown handler: ${handlerName}` });
        return;
      }
      const body = await readJson(req);
      const request = body as WorkerRequest;
      const raw = await handlers[handlerName](request);
      const response = normalizeWorkerResponse(raw);
      writeJson(res, 200, response);
    } catch (error) {
      writeJson(res, 500, { error: error instanceof Error ? error.message : String(error) });
    }
  });
  server.listen(options.port ?? 9001, options.host ?? "127.0.0.1");
  return server;
}

export function serveAgent(
  agentClass: AgentClass,
  options: { port?: number; host?: string } = {}
): Server {
  const dsl = compileAgent(agentClass);
  const agent = new agentClass();
  const handlerNames = new Set(dsl.skills.map((item) => item.handler));
  const server = createServer(async (req, res) => {
    try {
      if (req.method !== "POST" || !req.url?.startsWith("/_a2a/invoke/")) {
        writeJson(res, 404, { error: "not found" });
        return;
      }
      const handlerName = decodeURIComponent(req.url.slice("/_a2a/invoke/".length));
      if (!handlerNames.has(handlerName)) {
        writeJson(res, 404, { error: `unknown handler: ${handlerName}` });
        return;
      }
      const body = await readJson(req);
      const request = { ...(body as WorkerRequest), handler: handlerName };
      const response = await agent.dispatch(request);
      writeJson(res, 200, response);
    } catch (error) {
      writeJson(res, 500, { error: error instanceof Error ? error.message : String(error) });
    }
  });
  server.listen(options.port ?? 9001, options.host ?? "127.0.0.1");
  return server;
}

function normalizeSkill(declaration: SkillDeclaration): AgentDslSkill {
  validateSkill(declaration);
  return {
    name: declaration.name,
    description: declaration.description ?? "",
    handler: declaration.handler ?? declaration.name,
    tags: declaration.tags ?? [],
    scopes: declaration.scopes ?? [],
    stream: declaration.stream ?? false,
    policy: {
      timeout_seconds: declaration.policy?.timeout_seconds ?? null,
      idempotent: declaration.policy?.idempotent ?? false,
      max_retries: declaration.policy?.max_retries ?? 0,
      cost_class: declaration.policy?.cost_class ?? null,
      allow_scope_expansion: declaration.policy?.allow_scope_expansion ?? false,
      grant_mode: declaration.policy?.grant_mode ?? null,
      grant_allow_patterns: declaration.policy?.grant_allow_patterns ?? [],
      grant_deny_patterns: declaration.policy?.grant_deny_patterns ?? [],
      grant_outputs_prefix: declaration.policy?.grant_outputs_prefix ?? null,
      grant_write_prefixes: declaration.policy?.grant_write_prefixes ?? [],
      grant_ttl_seconds: declaration.policy?.grant_ttl_seconds ?? null,
      grant_run_timeout_seconds: declaration.policy?.grant_run_timeout_seconds ?? null,
      grant_approval_timeout_seconds: declaration.policy?.grant_approval_timeout_seconds ?? null,
      grant_scope_approval_timeout_seconds: declaration.policy?.grant_scope_approval_timeout_seconds ?? null
    },
    input_schema: declaration.input_schema,
    output_schema: declaration.output_schema
  };
}

function validateSkill(declaration: SkillDeclaration): void {
  if (!declaration.name) throw new Error("skill.name is required");
  if (declaration.input_schema?.type !== "object") {
    throw new Error(`skill ${declaration.name}: input_schema must be an object schema`);
  }
  if (typeof declaration.input_schema.properties !== "object" || declaration.input_schema.properties === null) {
    throw new Error(`skill ${declaration.name}: input_schema.properties is required`);
  }
  if (!Array.isArray(declaration.input_schema.required)) {
    throw new Error(`skill ${declaration.name}: input_schema.required must be an array`);
  }
  for (const key of declaration.input_schema.required) {
    if (typeof key !== "string") throw new Error(`skill ${declaration.name}: required values must be strings`);
    if (!(key in (declaration.input_schema.properties as Record<string, unknown>))) {
      throw new Error(`skill ${declaration.name}: required field ${key} missing from properties`);
    }
  }
  if (!declaration.output_schema || Object.keys(declaration.output_schema).length === 0) {
    throw new Error(`skill ${declaration.name}: output_schema is required`);
  }
}

function normalizeWorkerResponse(value: unknown): WorkerResponse {
  if (isObject(value) && "result" in value) {
    return value as WorkerResponse;
  }
  throw new Error("worker handler must return a Sidecar WorkerResponse object with a result field");
}

function isObject(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

async function readJson(req: IncomingMessage): Promise<unknown> {
  const chunks: Buffer[] = [];
  for await (const chunk of req) chunks.push(Buffer.from(chunk));
  const raw = Buffer.concat(chunks).toString("utf8");
  return raw ? JSON.parse(raw) : {};
}

function writeJson(res: ServerResponse, status: number, body: unknown): void {
  res.statusCode = status;
  res.setHeader("content-type", "application/json");
  res.end(JSON.stringify(body));
}
