import type { StandardSchemaV1 } from "@standard-schema/spec";

/**
 * Any Standard Schema compatible validator.
 */
export type StandardSchema = StandardSchemaV1<unknown, unknown>;

/**
 * Infer the parsed output type from a Standard Schema.
 */
export type InferOutput<T extends StandardSchemaV1> =
  StandardSchemaV1.InferOutput<T>;

/**
 * Validate data using a Standard Schema validator
 */
async function validateSchema<T>(
  schema: StandardSchemaV1<unknown, T>,
  data: unknown,
): Promise<T> {
  const result = await schema["~standard"].validate(data);
  if (result.issues?.length) {
    const message = result.issues
      .map((issue) => {
        const path =
          issue.path !== undefined
            ? Array.isArray(issue.path)
              ? issue.path.join(".")
              : String(issue.path)
            : "";
        return path ? `${path}: ${issue.message}` : issue.message;
      })
      .join("; ");
    throw new Error(`Validation failed: ${message}`);
  }
  if ("value" in result) {
    return result.value;
  }
  throw new Error("Invalid Standard Schema result: missing value");
}

/**
 * Check if data is valid according to a Standard Schema
 */
async function isValid<T>(
  schema: StandardSchemaV1<unknown, T>,
  data: unknown,
): Promise<boolean> {
  const result = await schema["~standard"].validate(data);
  return !result.issues?.length && "value" in result;
}

/**
 * Value object definition returned by `defineValueObject(...).build()`.
 */
export interface ValueObjectDef<
  Name extends string,
  Schema extends StandardSchema,
> {
  /** Name used for debugging and introspection. */
  name: Name;
  /** Standard Schema used to validate values. */
  schema: Schema;
  /**
   * Create a validated value from unknown input.
   *
   * This is async because Standard Schema validators may be async.
   */
  create(input: unknown): Promise<InferOutput<Schema>>;
  /**
   * Check whether an input is valid for this value object.
   */
  isValid(input: unknown): Promise<boolean>;
  /** Type-only alias for the inferred TypeScript type. Undefined at runtime. */
  Type: InferOutput<Schema>;
}

/**
 * Builder class for creating Value Objects.
 */
class ValueObjectBuilder<Name extends string, Schema extends StandardSchema> {
  constructor(
    private readonly cfg: {
      name: Name;
      schema?: Schema;
    },
  ) {}

  /**
   * Set the schema for this value object.
   */
  schema<S extends StandardSchema>(s: S): ValueObjectBuilder<Name, S> {
    return new ValueObjectBuilder<Name, S>({ ...this.cfg, schema: s });
  }

  /**
   * Finalize the value object definition.
   */
  build(): ValueObjectDef<Name, Schema> {
    if (!this.cfg.schema) {
      throw new Error(`Value object "${this.cfg.name}" is missing a schema`);
    }

    const schemaToUse = this.cfg.schema;

    const def: ValueObjectDef<Name, Schema> = {
      name: this.cfg.name,
      schema: schemaToUse,
      async create(input: unknown) {
        return validateSchema(schemaToUse, input);
      },
      async isValid(input: unknown) {
        return isValid(schemaToUse, input);
      },
      // Type is undefined at runtime - used only for TypeScript type inference via `typeof ValueObject.Type`
      Type: undefined as unknown as InferOutput<Schema>,
    };

    return def;
  }
}

/**
 * Create a new value object builder.
 *
 * Value objects are schema-backed primitives for domain concepts such as email
 * addresses or money values. Beignet does not add a runtime brand; the schema
 * output controls the runtime value.
 *
 * @example
 * ```ts
 * const Email = defineValueObject("Email")
 *   .schema(z.string().email())
 *   .build();
 *
 * type Email = typeof Email.Type;
 *
 * const email = await Email.create("test@example.com"); // OK
 * const isValid = await Email.isValid("test@example.com"); // true
 * ```
 */
export function defineValueObject<Name extends string>(name: Name) {
  // biome-ignore lint/suspicious/noExplicitAny: Initial schema type is never, will be set via .schema()
  return new ValueObjectBuilder<Name, never>({ name } as any);
}
