import { AlephaError } from "../errors/AlephaError.ts";
import { $inject } from "../primitives/$inject.ts";
import { JsonSchemaCodec } from "./JsonSchemaCodec.ts";
import { KeylessJsonSchemaCodec } from "./KeylessJsonSchemaCodec.ts";
import type { SchemaCodec } from "./SchemaCodec.ts";
import { SchemaValidator, type ValidateOptions } from "./SchemaValidator.ts";
import type { Static, StaticEncode, TSchema } from "./TypeProvider.ts";

export type Encoding = "object" | "string" | "binary";

export interface EncodeOptions<T extends Encoding = Encoding> {
  /**
   * The output encoding format:
   * - 'string': Returns JSON string
   * - 'binary': Returns Uint8Array (for protobuf, msgpack, etc.)
   *
   * @default "string"
   */
  as?: T;

  /**
   * The encoder to use (e.g., 'json', 'protobuf', 'msgpack')
   *
   * @default "json"
   */
  encoder?: string;

  /**
   * Validation options to apply before encoding.
   */
  validation?: ValidateOptions | false;
}

export type EncodeResult<
  T extends TSchema,
  E extends Encoding,
> = E extends "string"
  ? string
  : E extends "binary"
    ? Uint8Array
    : StaticEncode<T>;

export interface DecodeOptions {
  /**
   * The encoder to use (e.g., 'json', 'protobuf', 'msgpack')
   *
   * @default "json"
   */
  encoder?: string;

  /**
   * Validation options to apply before encoding.
   */
  validation?: ValidateOptions | false;
}

/**
 * CodecManager manages multiple codec formats and provides a unified interface
 * for encoding and decoding data with different formats.
 */
export class CodecManager {
  protected readonly codecs: Map<string, SchemaCodec> = new Map();
  protected readonly jsonCodec = $inject(JsonSchemaCodec);
  protected readonly keylessCodec = $inject(KeylessJsonSchemaCodec);
  protected readonly schemaValidator = $inject(SchemaValidator);

  public default = "json";

  constructor() {
    // Register default JSON codec
    this.register({
      name: "json",
      codec: this.jsonCodec,
      default: true,
    });

    // Register keyless JSON codec (smaller, faster decoding)
    this.register({
      name: "keyless",
      codec: this.keylessCodec,
    });
  }

  /**
   * Register a new codec format.
   */
  public register(opts: CodecRegisterOptions): void {
    this.codecs.set(opts.name, opts.codec);
    if (opts.default) {
      this.default = opts.name;
    }
  }

  /**
   * Get a specific codec by name.
   *
   * @param name - The name of the codec
   * @returns The codec instance
   * @throws {AlephaError} If the codec is not found
   */
  public getCodec(name: string): SchemaCodec {
    const codec = this.codecs.get(name);
    if (!codec) {
      throw new AlephaError(
        `Codec "${name}" not found. Available codecs: ${Array.from(this.codecs.keys()).join(", ")}`,
      );
    }
    return codec;
  }

  /**
   * Encode data using the specified codec and output format.
   */
  public encode<T extends TSchema, E extends Encoding = "object">(
    schema: T,
    value: unknown,
    options?: EncodeOptions<E>,
  ): EncodeResult<T, E> {
    const codec = this.getCodec(options?.encoder ?? this.default);
    const as = options?.as ?? "object";

    if (options?.validation !== false) {
      value = this.schemaValidator.validate(schema, value, options?.validation);
    }

    if (as === "object") {
      // Return the validated object as-is
      return value as EncodeResult<T, E>;
    }

    if (as === "binary") {
      // not used by JSON, but for other codecs like Protobuf, MsgPack, etc.
      return codec.encodeToBinary(schema, value as Static<T>) as EncodeResult<
        T,
        E
      >;
    }

    // encode directly to string
    return codec.encodeToString(schema, value as Static<T>) as EncodeResult<
      T,
      E
    >;
  }

  /**
   * Decode data using the specified codec.
   */
  public decode<T extends TSchema>(
    schema: T,
    data: any,
    options?: DecodeOptions,
  ): Static<T> {
    const encoderName = options?.encoder ?? this.default;
    const codec = this.getCodec(encoderName);
    let value = codec.decode(schema, data);

    if (options?.validation !== false) {
      value = this.schemaValidator.validate(schema, value, options?.validation);
    }

    return value as Static<T>;
  }

  /**
   * Validate decoded data against the schema.
   *
   * This is automatically called before encoding or after decoding.
   */
  public validate<T extends TSchema>(
    schema: T,
    value: unknown,
    options?: ValidateOptions,
  ): Static<T> {
    return this.schemaValidator.validate(schema, value, options);
  }
}

// ---------------------------------------------------------------------------------------------------------------------

export interface CodecRegisterOptions {
  name: string;
  codec: SchemaCodec;
  default?: boolean;
}
