// @lifecycle canonical - Declares zod schemas for parsed operator arguments.
import { z, type ZodTypeAny } from 'zod';

import type { PromptArgument } from '../../../shared/types/index.js';
import type { ConvertedPrompt } from '../types.js';

export interface SchemaValidationIssue {
  argument: string;
  message: string;
  code?: string;
}

export interface SchemaValidationResult {
  success: boolean;
  issues: SchemaValidationIssue[];
}

export type PromptSchemaOverrides = Record<string, ZodTypeAny>;

export class ArgumentSchemaValidator {
  private readonly cache = new Map<string, ZodTypeAny>();

  constructor(private readonly overrides: PromptSchemaOverrides = {}) {}

  validate(prompt: ConvertedPrompt, args: Record<string, any>): SchemaValidationResult {
    if (!prompt.arguments.length) {
      return { success: true, issues: [] };
    }

    const schema = this.getSchema(prompt);
    if (!schema) {
      return { success: true, issues: [] };
    }

    const parsed = schema.safeParse(args);
    if (parsed.success) {
      return { success: true, issues: [] };
    }

    const issues: SchemaValidationIssue[] = parsed.error.issues.map((issue) => ({
      argument: issue.path?.[0]?.toString() ?? '',
      message: issue.message,
      code: issue.code,
    }));

    return {
      success: false,
      issues,
    };
  }

  private getSchema(prompt: ConvertedPrompt): ZodTypeAny | null {
    const override = this.overrides[prompt.id];
    if (override) {
      return override;
    }

    if (this.cache.has(prompt.id)) {
      return this.cache.get(prompt.id) ?? null;
    }

    if (!prompt.arguments.length) {
      return null;
    }

    const schema = this.buildSchema(prompt);
    if (schema) {
      this.cache.set(prompt.id, schema);
    }

    return schema;
  }

  private buildSchema(prompt: ConvertedPrompt): ZodTypeAny | null {
    if (!prompt.arguments.length) {
      return null;
    }

    const shape: Record<string, ZodTypeAny> = {};
    for (const arg of prompt.arguments) {
      let schema = this.createArgumentSchema(arg);
      schema = arg.required ? schema : schema.optional();
      shape[arg.name] = schema;
    }

    return z.object(shape).passthrough();
  }

  private createArgumentSchema(arg: PromptArgument): ZodTypeAny {
    const type = arg.type ?? 'string';
    switch (type) {
      case 'number':
        return this.applyCommonConstraints(
          arg,
          z.coerce.number({ invalid_type_error: `Argument ${arg.name} must be a number` })
        );
      case 'boolean':
        return this.applyCommonConstraints(
          arg,
          z.coerce.boolean({ invalid_type_error: `Argument ${arg.name} must be a boolean` })
        );
      case 'array':
        return this.createArraySchema(arg);
      case 'object':
        return this.createObjectSchema(arg);
      case 'string':
      default:
        return this.applyStringConstraints(
          arg,
          z.string({ invalid_type_error: `Argument ${arg.name} must be a string` })
        );
    }
  }

  private createArraySchema(arg: PromptArgument): ZodTypeAny {
    const target = z.array(z.any(), {
      invalid_type_error: `Argument ${arg.name} must be an array`,
    });

    const schema = z.preprocess((val) => {
      if (typeof val === 'string') {
        try {
          const parsed = JSON.parse(val);
          return parsed;
        } catch {
          return val;
        }
      }
      return val;
    }, target);

    return this.applyCommonConstraints(arg, schema);
  }

  private createObjectSchema(arg: PromptArgument): ZodTypeAny {
    const target = z.record(z.any(), {
      invalid_type_error: `Argument ${arg.name} must be an object`,
    });

    const schema = z.preprocess((val) => {
      if (typeof val === 'string') {
        try {
          const parsed = JSON.parse(val);
          return parsed;
        } catch {
          return val;
        }
      }
      return val;
    }, target);

    return this.applyCommonConstraints(arg, schema);
  }

  /**
   * Apply common constraints to any schema type.
   * Note: allowedValues was removed in v3.0.0 - LLM handles semantic variation better
   * than strict enum enforcement.
   */
  private applyCommonConstraints<T extends ZodTypeAny>(
    _arg: PromptArgument,
    schema: T
  ): ZodTypeAny {
    // Currently no common constraints apply to all types.
    // minLength/maxLength/pattern are string-specific and handled in applyStringConstraints.
    return schema;
  }

  private applyStringConstraints(arg: PromptArgument, schema: z.ZodString): ZodTypeAny {
    let refined: z.ZodString = schema;
    const { validation } = arg;

    if (validation?.pattern) {
      try {
        const pattern = new RegExp(validation.pattern);
        refined = refined.regex(pattern, {
          message: `Value must match pattern ${validation.pattern}`,
        });
      } catch {
        // Ignore invalid regex patterns to avoid breaking parsing
      }
    }

    if (typeof validation?.minLength === 'number') {
      refined = refined.min(validation.minLength, {
        message: `Value must contain at least ${validation.minLength} characters`,
      });
    }

    if (typeof validation?.maxLength === 'number') {
      refined = refined.max(validation.maxLength, {
        message: `Value must contain no more than ${validation.maxLength} characters`,
      });
    }

    // Note: allowedValues support was removed in v3.0.0
    // LLM handles semantic variation (e.g., "urgent" vs "high") better than strict enums

    return refined;
  }
}
