/**
 * Agent + Chat validation schemas.
 *
 * `AgentChatParamsSchema` is the LiteLLM-style passthrough used by both
 * `agent.defaultParams` (stored on the agent row) and the per-call request
 * body. Resolution order at chat time: request body → agent.defaultParams →
 * adapter default. `extra` is the escape hatch for provider-specific knobs;
 * adapters cherry-pick what they understand and ignore the rest.
 */
import { z } from 'zod';

/** OpenAI tool-choice schema, matching what we'll thread through to adapters. */
const ToolChoiceSchema = z.union([
  z.literal('auto'),
  z.literal('none'),
  z.literal('required'),
  z.object({
    type: z.literal('function'),
    function: z.object({ name: z.string().min(1) }),
  }),
]);

const ResponseFormatSchema = z
  .object({
    type: z.enum(['text', 'json_object', 'json_schema']),
  })
  .passthrough();

/**
 * The LiteLLM-style chat parameter set. Every field is optional — both
 * `defaultParams` (stored on Agent) and per-call overrides reuse this shape.
 */
export const AgentChatParamsSchema = z
  .object({
    // Sampling
    temperature: z.number().min(0).max(2).optional(),
    top_p: z.number().min(0).max(1).optional(),
    top_k: z.number().int().min(0).optional(),
    max_tokens: z.number().int().positive().optional(),
    stop: z.union([z.string(), z.array(z.string()).max(4)]).optional(),
    presence_penalty: z.number().min(-2).max(2).optional(),
    frequency_penalty: z.number().min(-2).max(2).optional(),
    seed: z.number().int().optional(),
    response_format: ResponseFormatSchema.optional(),
    // Persona overrides
    systemOverride: z.string().optional(),
    systemAppend: z.string().optional(),
    // Tools
    tool_choice: ToolChoiceSchema.optional(),
    tools_allowlist: z.array(z.string().min(1)).optional(),
    // Provider escape hatch
    extra: z.record(z.unknown()).optional(),
  })
  .strict();

export type AgentChatParams = z.infer<typeof AgentChatParamsSchema>;

/** Optional named pointer at an Llm row. Mirrors `apiKeyRef` on Llm. */
const LlmRefSchema = z.union([
  z.object({ name: z.string().min(1) }),
  z.object({ id: z.string().min(1) }),
]);
const ProjectRefSchema = z.object({ name: z.string().min(1) });
/**
 * Personality reference is by name only (per-agent unique constraint).
 * `null` clears the agent's defaultPersonalityId on update.
 */
const PersonalityRefSchema = z.object({ name: z.string().min(1) });

const NAME_RE = /^[a-z0-9-]+$/;

export const CreateAgentSchema = z.object({
  name: z
    .string()
    .min(1)
    .max(100)
    .regex(NAME_RE, 'Name must be lowercase alphanumeric with hyphens'),
  description: z.string().max(500).default(''),
  systemPrompt: z.string().max(64_000).default(''),
  llm: LlmRefSchema,
  project: ProjectRefSchema.optional(),
  proxyModelName: z.string().min(1).optional(),
  defaultParams: AgentChatParamsSchema.default({}),
  extras: z.record(z.unknown()).default({}),
});

export const UpdateAgentSchema = z.object({
  description: z.string().max(500).optional(),
  systemPrompt: z.string().max(64_000).optional(),
  llm: LlmRefSchema.optional(),
  project: ProjectRefSchema.nullable().optional(),
  defaultPersonality: PersonalityRefSchema.nullable().optional(),
  proxyModelName: z.string().min(1).nullable().optional(),
  defaultParams: AgentChatParamsSchema.optional(),
  extras: z.record(z.unknown()).optional(),
});

/** Body schema for `POST /api/v1/agents/:name/chat`. */
// `.merge()` does NOT inherit `.strict()` from `AgentChatParamsSchema`, so we
// re-apply it on the merged schema. Without this, typo'd request fields (e.g.
// `temprature` instead of `temperature`) silently fall through and the agent
// uses the default — debuggable only by reading the LLM call payload.
export const AgentChatRequestSchema = AgentChatParamsSchema.merge(
  z.object({
    threadId: z.string().min(1).optional(),
    message: z.string().min(1).optional(),
    messages: z
      .array(
        z.object({
          role: z.enum(['system', 'user', 'assistant', 'tool']),
          content: z.string(),
          tool_call_id: z.string().optional(),
        }),
      )
      .optional(),
    stream: z.boolean().optional(),
    /**
     * Optional personality overlay for this turn. Looked up by name on the
     * agent's own personality set (per-agent unique). Falls back to the
     * agent's `defaultPersonalityId` when omitted.
     */
    personality: z.string().min(1).optional(),
  }),
).strict().refine((v) => v.message !== undefined || (v.messages?.length ?? 0) > 0, {
  message: 'Either `message` or `messages` is required',
});

/**
 * Project-scoped chat request (`POST /api/v1/projects/:name/chat`). Same
 * LiteLLM-style params as an agent chat, minus the agent-only `personality`
 * overlay, plus `allowSecrets` to opt into the gated get_secret tool.
 */
export const ProjectChatRequestSchema = AgentChatParamsSchema.merge(
  z.object({
    threadId: z.string().min(1).optional(),
    message: z.string().min(1).optional(),
    messages: z
      .array(
        z.object({
          role: z.enum(['system', 'user', 'assistant', 'tool']),
          content: z.string(),
          tool_call_id: z.string().optional(),
        }),
      )
      .optional(),
    stream: z.boolean().optional(),
    /** Opt into the gated get_secret tool (still requires the caller's view:secrets). */
    allowSecrets: z.boolean().optional(),
  }),
).strict().refine((v) => v.message !== undefined || (v.messages?.length ?? 0) > 0, {
  message: 'Either `message` or `messages` is required',
});

export type CreateAgentInput = z.infer<typeof CreateAgentSchema>;
export type UpdateAgentInput = z.infer<typeof UpdateAgentSchema>;
export type AgentChatRequest = z.infer<typeof AgentChatRequestSchema>;
export type ProjectChatRequest = z.infer<typeof ProjectChatRequestSchema>;
