import { z } from 'zod';

export const ServerStatus = z.enum([
  'draft',
  'queued',
  'generating',
  'building',
  'deploying',
  'live',
  'failed',
  'paused',
]);
export type ServerStatus = z.infer<typeof ServerStatus>;

export const BuildStatus = z.enum([
  'queued',
  'generating',
  'building',
  'deploying',
  'success',
  'failed',
  'cancelled',
]);
export type BuildStatus = z.infer<typeof BuildStatus>;

export const Plan = z.enum(['hobby', 'pro', 'team', 'enterprise']);
export type Plan = z.infer<typeof Plan>;

// ---- Generator output spec (what Claude returns) ----

export const ToolParam = z
  .object({
    type: z.enum(['string', 'number', 'boolean', 'array', 'object']),
    description: z.string().optional(),
    required: z.boolean().optional(),
    enum: z.array(z.string()).optional(),
  })
  .passthrough();

export const ToolSpec = z.object({
  name: z
    .string()
    .min(1)
    .max(64)
    .regex(/^[a-z][a-z0-9_]*$/, 'snake_case identifier required'),
  description: z.string().min(1).max(2000),
  inputSchema: z.record(z.string(), ToolParam),
  implementation: z.string().min(1).max(20_000),
});
export type ToolSpec = z.infer<typeof ToolSpec>;

export const ResourceSpec = z.object({
  uri: z.string().min(1),
  name: z.string().min(1),
  description: z.string().optional(),
  mimeType: z.string().optional(),
  implementation: z.string().min(1),
});
export type ResourceSpec = z.infer<typeof ResourceSpec>;

export const PromptSpec = z.object({
  name: z.string().min(1),
  description: z.string().optional(),
  arguments: z
    .array(z.object({ name: z.string(), description: z.string().optional(), required: z.boolean().optional() }))
    .optional(),
  template: z.string().min(1),
});
export type PromptSpec = z.infer<typeof PromptSpec>;

// Dependency specifiers come from untrusted LLM output and are merged into the
// build's package.json, then `npm install`-ed inside `docker build` on the
// SHARED host. Restrict to npm-registry semver ranges only: no `git+`, `http(s):`,
// `file:` or tarball-URL specifiers (which fetch/checkout+run arbitrary code at
// install time), and valid (optionally-scoped) npm package names. Combined with
// `--ignore-scripts` in the runner Dockerfile this closes the build-time RCE. (GEN-001)
const DepName = z
  .string()
  .max(214)
  .regex(/^(@[a-z0-9][\w.-]*\/)?[a-z0-9][\w.-]*$/i, 'invalid npm package name');
const DepRange = z
  .string()
  .max(64)
  .regex(/^([\^~]?\d+(\.\d+){0,2}(-[\w.]+)?|\*|latest)$/, 'must be a plain semver range');
export const DependencyMap = z.record(DepName, DepRange);

// Secret KEYS become `-e KEY=VALUE` docker run args and the runtime env of the
// tenant container. Constrain to UPPER_SNAKE_CASE (matches requiredSecrets) and
// reject names that could hijack the Node runtime/loader if the container
// hardening ever regressed. Values stay free-form. (GEN-003)
const RESERVED_ENV = new Set(['PATH', 'LD_PRELOAD', 'LD_LIBRARY_PATH', 'NODE_ENV']);
const SecretKey = z
  .string()
  .regex(/^[A-Z][A-Z0-9_]*$/, 'UPPER_SNAKE_CASE env var name required')
  .refine((k) => !RESERVED_ENV.has(k) && !k.startsWith('NODE_'), 'reserved env var name');
export const SecretMap = z.record(SecretKey, z.string());

export const GeneratorSpec = z.object({
  name: z.string().min(1).max(128),
  description: z.string().max(2000).optional(),
  tools: z.array(ToolSpec).min(1).max(50),
  resources: z.array(ResourceSpec).max(50).default([]),
  prompts: z.array(PromptSpec).max(50).default([]),
  requiredSecrets: z.array(z.string().regex(/^[A-Z][A-Z0-9_]*$/)).max(30).default([]),
  scopes: z.array(z.string()).max(50).default([]),
  dependencies: DependencyMap.default({}),
});
export type GeneratorSpec = z.infer<typeof GeneratorSpec>;

// ---- Build stream events ----

export const BuildEvent = z.discriminatedUnion('type', [
  z.object({
    type: z.literal('status'),
    status: BuildStatus,
    at: z.string(),
  }),
  z.object({
    type: z.literal('log'),
    level: z.enum(['info', 'warn', 'error']),
    message: z.string(),
    at: z.string(),
  }),
  z.object({
    type: z.literal('done'),
    status: BuildStatus,
    serverId: z.string().uuid(),
    publicUrl: z.string().nullable(),
    at: z.string(),
  }),
  z.object({
    type: z.literal('error'),
    message: z.string(),
    at: z.string(),
  }),
]);
export type BuildEvent = z.infer<typeof BuildEvent>;

// ---- API request payloads ----

export const SpecEditTool = z.object({
  name: z
    .string()
    .min(1)
    .max(64)
    .regex(/^[a-z][a-z0-9_]*$/, 'snake_case identifier required'),
  description: z.string().min(1).max(2000),
  inputSchema: z.record(z.string(), ToolParam),
});
export type SpecEditTool = z.infer<typeof SpecEditTool>;

export const SpecEdit = z.object({
  tools: z.array(SpecEditTool).min(1).max(50),
  requiredSecrets: z.array(z.string().regex(/^[A-Z][A-Z0-9_]*$/)).max(30).default([]),
});
export type SpecEdit = z.infer<typeof SpecEdit>;

export const CreateServerInput = z.object({
  name: z.string().min(1).max(128),
  slug: z
    .string()
    .min(1)
    .max(64)
    .regex(/^[a-z][a-z0-9-]*$/, 'lowercase, hyphenated'),
  prompt: z.string().min(10).max(8000),
  secrets: SecretMap.default({}),
  previewId: z.string().min(1).max(64).optional(),
  specEdit: SpecEdit.optional(),
  templateId: z.string().uuid().optional(),
});
export type CreateServerInput = z.infer<typeof CreateServerInput>;

export const PreviewInput = z.object({
  prompt: z.string().min(10).max(8000),
});
export type PreviewInput = z.infer<typeof PreviewInput>;

export const PreviewResult = z.object({
  previewId: z.string(),
  source: z.enum(['claude', 'mock']),
  spec: z.object({
    name: z.string(),
    description: z.string().optional(),
    tools: z.array(
      z.object({
        name: z.string(),
        description: z.string(),
        inputSchema: z.record(z.string(), z.unknown()),
      }),
    ),
    requiredSecrets: z.array(z.string()),
    scopes: z.array(z.string()),
  }),
});
export type PreviewResult = z.infer<typeof PreviewResult>;

export const IterateServerInput = z.object({
  prompt: z.string().min(10).max(8000),
  secrets: SecretMap.default({}),
});
export type IterateServerInput = z.infer<typeof IterateServerInput>;

// ---- Install snippets ----

export const InstallTarget = z.enum([
  'claude-desktop',
  'claude-code',
  'cursor',
  'vscode',
  'chatgpt',
  'codex',
  'raw-url',
]);
export type InstallTarget = z.infer<typeof InstallTarget>;

// ---- Prompt secret guard ----
// Unambiguous provider key/token shapes. Used to reject a prompt that contains
// a real credential BEFORE it is sent to the LLM — keys must never reach the
// model. Kept tight (provider prefixes / structural markers) so normal prompts
// don't trip it. Shared by the API preview endpoints and the web wizard.
const PROMPT_SECRET_PATTERNS: ReadonlyArray<{ name: string; re: RegExp }> = [
  { name: 'an Anthropic key', re: /\bsk-ant-[A-Za-z0-9_-]{20,}/ },
  { name: 'an OpenAI project key', re: /\bsk-proj-[A-Za-z0-9_-]{20,}/ },
  { name: 'an OpenAI key', re: /\bsk-[A-Za-z0-9]{32,}\b/ },
  { name: 'a GitHub token', re: /\bgh[posru]_[A-Za-z0-9]{30,}\b/ },
  { name: 'an AWS access key', re: /\bAKIA[0-9A-Z]{16}\b/ },
  { name: 'a Google API key', re: /\bAIza[0-9A-Za-z_-]{35}\b/ },
  { name: 'a Slack token', re: /\bxox[baprs]-[A-Za-z0-9-]{10,}/ },
  { name: 'a Stripe secret key', re: /\b(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{20,}/ },
  { name: 'a Stripe publishable key', re: /\bpk_(?:live|test)_[A-Za-z0-9]{20,}/ },
  { name: 'a JWT / bearer token', re: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/ },
  { name: 'a private key block', re: /-----BEGIN (?:RSA |EC |OPENSSH |PGP )?PRIVATE KEY-----/ },
];

/** First secret-like token found in the text, else null. */
export function findSecretInPrompt(text: string): string | null {
  for (const { name, re } of PROMPT_SECRET_PATTERNS) {
    if (re.test(text)) return name;
  }
  return null;
}
