import { faker } from "@faker-js/faker";
import type {
  StaticDecode,
  TArray,
  TBoolean,
  TInteger,
  TNumber,
  TObject,
  TRecord,
  TSchema,
  TString,
} from "alepha";
import { z } from "alepha";

export interface FakeOptions {
  /**
   * Faker locale to use for generating fake data.
   * @default "en"
   */
  locale?: string;

  /**
   * Seed for deterministic fake data generation.
   */
  seed?: number;

  /**
   * Probability (0-1) that an optional field will be undefined.
   * @default 0.3
   */
  optionalProbability?: number;

  /**
   * Probability (0-1) that a nullable field will be null.
   * @default 0.2
   */
  nullableProbability?: number;

  /**
   * Default number of items to generate for arrays when maxItems is not specified.
   * @default 5
   */
  defaultArrayLength?: number;

  /**
   * Maximum number of items to generate for arrays.
   * Set to Infinity to respect schema's maxItems without cap.
   * @default 20
   */
  maxArrayLength?: number;

  /**
   * Default number of entries to generate for record types.
   * @default { min: 2, max: 5 }
   */
  defaultRecordEntries?: { min: number; max: number };
}

/**
 * Generate fake data from TypeBox schemas using faker.js.
 *
 * @example
 * ```ts
 * const fake = new FakeProvider();
 * const userSchema = z.object({
 *   id: z.uuid(),
 *   name: z.text(),
 *   email: z.email(),
 * });
 * const fakeUser = fake.generate(userSchema);
 *
 * // Configure options
 * fake.configure({ seed: 99999, optionalProbability: 0.5 });
 * ```
 */
export class FakeProvider {
  protected readonly faker = faker;
  protected readonly guard = z.schema;
  protected options: Required<FakeOptions> = {
    locale: "en",
    seed: 12345,
    optionalProbability: 0.3,
    nullableProbability: 0.2,
    defaultArrayLength: 5,
    maxArrayLength: 20,
    defaultRecordEntries: { min: 2, max: 5 },
  };

  constructor() {
    this.faker.seed(this.options.seed);
  }

  /**
   * Configure generation options.
   * If a seed is provided, the faker instance is reseeded.
   */
  public configure(options: Partial<FakeOptions>): this {
    const oldSeed = this.options.seed;

    this.options = {
      ...this.options,
      ...options,
      // Merge defaultRecordEntries if provided
      defaultRecordEntries: options.defaultRecordEntries
        ? {
            ...this.options.defaultRecordEntries,
            ...options.defaultRecordEntries,
          }
        : this.options.defaultRecordEntries,
    };

    // Reseed faker if seed is provided (allows resetting to same seed)
    if (options.seed !== undefined) {
      this.faker.seed(options.seed);
    }

    return this;
  }

  /**
   * Generate fake data matching the given TypeBox schema.
   *
   * For object schemas, property names are used to generate contextually
   * appropriate values. The following field name patterns are recognized
   * (case-insensitive, underscores and hyphens are ignored):
   *
   * **String fields:**
   * - `email` → email address
   * - `firstName`, `first` → first name
   * - `lastName`, `last` → last name
   * - `fullName`, `name` → full name
   * - `username` → username
   * - `phone`, `mobile` → phone number
   * - `address` → street address
   * - `city` → city name
   * - `country` → country name
   * - `state`, `province` → state/province
   * - `zip`, `postal` → postal code
   * - `company`, `organization` → company name
   * - `job`, `title`, `position` → job title
   * - `url`, `website` → URL
   * - `avatar`, `image`, `photo` → avatar URL
   * - `color`, `colour` → color name
   * - `bio`, `about`, `description` → bio text
   *
   * **Number/Integer fields:**
   * - `age` → 18-99
   * - `year` → past year
   * - `month` → 1-12
   * - `day` → 1-31
   * - `price`, `amount`, `cost` → monetary value
   *
   * @example
   * ```ts
   * const fake = new FakeProvider();
   * const schema = z.object({
   *   user_name: z.string(),  // generates username
   *   firstName: z.string(),  // generates first name
   *   "e-mail": z.string(),   // generates email
   * });
   * const result = fake.generate(schema);
   * ```
   */
  public generate<T extends TSchema>(schema: T): StaticDecode<T> {
    return this.generateValue(schema) as StaticDecode<T>;
  }

  /**
   * Generate multiple fake data items.
   */
  public generateMany<T extends TSchema>(
    schema: T,
    count: number,
  ): StaticDecode<T>[] {
    return Array.from({ length: count }, () => this.generate(schema));
  }

  protected generateValue(schema: TSchema): unknown {
    const s = schema as any;

    // Handle optional — peel one layer and recurse
    if (this.guard.isOptional(schema)) {
      if (
        this.faker.datatype.boolean({
          probability: this.options.optionalProbability,
        })
      ) {
        return undefined;
      }
      return this.generateValue(s.unwrap());
    }

    // Handle nullable (zod ZodNullable — distinct from a union-with-null)
    if (this.guard.isNullable(schema)) {
      if (
        this.faker.datatype.boolean({
          probability: this.options.nullableProbability,
        })
      ) {
        return null;
      }
      return this.generateValue(s.unwrap());
    }

    // Handle default — generate a value for the wrapped type
    if (z.schema.isDefault(s)) {
      return this.generateValue(s.unwrap() as TSchema);
    }

    // Handle enum
    if (this.guard.isEnum(schema)) {
      const values = z.schema.enumValues(schema);
      return values[this.faker.number.int({ min: 0, max: values.length - 1 })];
    }

    // Handle union (a union containing null behaves like nullable)
    if (this.guard.isUnion(schema)) {
      const members = (s.anyOf ?? s.options ?? []) as TSchema[];
      const hasNull = members.some((m) => this.guard.isNull(m));
      if (hasNull) {
        if (
          this.faker.datatype.boolean({
            probability: this.options.nullableProbability,
          })
        ) {
          return null;
        }
        const nonNull = members.filter((m) => !this.guard.isNull(m));
        return this.generateValue(
          nonNull[
            this.faker.number.int({ min: 0, max: nonNull.length - 1 })
          ] as TSchema,
        );
      }
      return this.generateValue(
        members[
          this.faker.number.int({ min: 0, max: members.length - 1 })
        ] as TSchema,
      );
    }

    // Handle null
    if (this.guard.isNull(schema)) {
      return null;
    }

    // Handle undefined / void
    if (this.guard.isUndefined(schema) || this.guard.isVoid(schema)) {
      return undefined;
    }

    // Handle literal
    if (this.guard.isLiteral(schema)) {
      return s.value;
    }

    // Handle bigint (a validated string) — must precede the plain-string check
    if (this.guard.isBigInt(schema)) {
      return this.generateBigInt(schema as TString);
    }

    // Handle string
    if (this.guard.isString(schema)) {
      return this.generateString(schema as TString);
    }

    // Handle integer — must precede the plain-number check
    if (this.guard.isInteger(schema)) {
      return this.generateInteger(schema as TInteger);
    }

    // Handle number
    if (this.guard.isNumber(schema)) {
      return this.generateNumber(schema as TNumber);
    }

    // Handle boolean
    if (this.guard.isBoolean(schema)) {
      return this.generateBoolean(schema as TBoolean);
    }

    // Handle array
    if (this.guard.isArray(schema)) {
      return this.generateArray(schema as TArray);
    }

    // Handle object
    if (this.guard.isObject(schema)) {
      return this.generateObject(schema as TObject);
    }

    // Handle record
    if (this.guard.isRecord(schema)) {
      return this.generateRecord(schema as TRecord);
    }

    // Handle tuple
    if (this.guard.isTuple(schema)) {
      const items = (s.items ?? []) as TSchema[];
      return items.map((item) => this.generateValue(item));
    }

    // Handle any / unknown
    if (this.guard.isAny(schema) || this.guard.isUnsafe(schema)) {
      const types = ["string", "number", "boolean"];
      const kind =
        types[this.faker.number.int({ min: 0, max: types.length - 1 })];
      if (kind === "number") return this.faker.number.float();
      if (kind === "boolean") return this.faker.datatype.boolean();
      return this.faker.lorem.word();
    }

    // Fallback
    return this.faker.lorem.word();
  }

  protected generateString(schema: TString): string {
    const schemaAny = schema as any;
    const format = z.schema.format(schema);
    const metaObj = (schemaAny.meta?.() ?? {}) as Record<string, any>;
    const rawPattern = metaObj.pattern;
    const pattern =
      typeof rawPattern === "string"
        ? rawPattern
        : (rawPattern?.source ?? undefined);
    const minLength = schemaAny.minLength ?? undefined;
    const maxLength = schemaAny.maxLength ?? undefined;

    // Handle specific formats
    if (format) {
      switch (format) {
        case "uuid":
          return this.faker.string.uuid();
        case "email":
          return this.faker.internet.email();
        case "url":
          return this.faker.internet.url();
        case "date-time":
          return this.faker.date.recent().toISOString();
        case "date":
          return this.faker.date.recent().toISOString().split("T")[0];
        case "time":
          return this.faker.date.recent().toISOString().split("T")[1];
        case "bigint":
          return this.faker.number
            .bigInt({ min: -9007199254740991n, max: 9007199254740991n })
            .toString();
        case "binary":
          return btoa(this.faker.string.alphanumeric(20));
      }
    }

    // Handle E.164 phone pattern
    if (pattern?.includes("\\+[1-9]\\d{1,14}")) {
      // Generate a proper E.164 format: + followed by 1-15 digits
      const countryCode = this.faker.number.int({ min: 1, max: 999 });
      const phoneNumber = this.faker.string.numeric({
        length: this.faker.number.int({ min: 7, max: 11 }),
      });
      return `+${countryCode}${phoneNumber}`;
    }

    // Handle BCP 47 language tag pattern
    if (pattern?.includes("[a-z]{2,3}")) {
      const locales = ["en", "en-US", "fr", "fr-CA", "es", "de", "it", "ja"];
      return locales[
        this.faker.number.int({ min: 0, max: locales.length - 1 })
      ];
    }

    // Handle snake_case pattern
    if (pattern === "^[A-Z_-]+$") {
      return this.faker.lorem
        .word()
        .toUpperCase()
        .replace(/[^A-Z]/g, "_");
    }

    // Generate text based on length constraints
    let text: string;
    if (maxLength !== undefined) {
      if (maxLength <= 10) {
        text = this.faker.lorem.word();
      } else if (maxLength <= 64) {
        text = this.faker.lorem.words(2);
      } else if (maxLength <= 255) {
        text = this.faker.lorem.sentence();
      } else if (maxLength <= 1024) {
        text = this.faker.lorem.paragraph(1);
      } else {
        text = this.faker.lorem.paragraphs(3);
      }
    } else {
      text = this.faker.lorem.sentence();
    }

    // Ensure min/max length constraints
    if (minLength !== undefined && text.length < minLength) {
      text = text.padEnd(minLength, " ");
    }
    if (maxLength !== undefined && text.length > maxLength) {
      text = text.slice(0, maxLength);
    }

    return text;
  }

  protected generateNumber(schema: TNumber): number {
    const schemaAny = schema as any;
    // zod exposes numeric bounds as `.minValue` / `.maxValue` (number | null).
    const min = schemaAny.minValue ?? -1000000;
    const max = schemaAny.maxValue ?? 1000000;
    // `.multipleOf` is a builder method on zod schemas, not a value — only use
    // it if it surfaces as an actual number.
    const multipleOf =
      typeof schemaAny.multipleOf === "number"
        ? schemaAny.multipleOf
        : undefined;

    let value = this.faker.number.float({ min, max });

    if (multipleOf != null) {
      value = Math.round(value / multipleOf) * multipleOf;
    }

    return value;
  }

  protected generateInteger(schema: TInteger): number {
    const schemaAny = schema as any;
    const min = schemaAny.minValue ?? -2147483647;
    const max = schemaAny.maxValue ?? 2147483647;

    return this.faker.number.int({ min, max });
  }

  protected generateBigInt(schema: TString): string {
    return this.faker.number
      .bigInt({ min: -9007199254740991n, max: 9007199254740991n })
      .toString();
  }

  protected generateBoolean(_schema: TBoolean): boolean {
    return this.faker.datatype.boolean();
  }

  protected generateArray(schema: TArray): unknown[] {
    // Native zod stores `.min()`/`.max()` length bounds in `_zod.def.checks`,
    // not in `.meta()`. Read those (falling back to legacy meta minItems/maxItems).
    const def = (schema as any)._zod?.def;
    const checks = (def?.checks ?? []) as any[];
    const readBound = (check: string, key: string): number | undefined => {
      for (const c of checks) {
        const d = c?._zod?.def;
        if (d?.check === check) return d[key];
      }
      return undefined;
    };
    const metaObj = ((schema as any).meta?.() ?? {}) as Record<string, any>;
    const minItems =
      readBound("min_length", "minimum") ?? metaObj.minItems ?? 0;
    const maxItems = Math.min(
      readBound("max_length", "maximum") ??
        metaObj.maxItems ??
        this.options.defaultArrayLength,
      this.options.maxArrayLength,
    );
    const length = this.faker.number.int({
      min: minItems,
      max: Math.max(minItems, maxItems),
    });

    const element = (schema as any).element ?? def?.element;
    return Array.from({ length }, () => this.generateValue(element));
  }

  protected generateObject(schema: TObject): Record<string, unknown> {
    const result: Record<string, unknown> = {};

    for (const [key, propSchema] of Object.entries(schema.properties)) {
      result[key] = this.generateValueWithContext(propSchema as TSchema, key);
    }

    return result;
  }

  /**
   * Generate a value with context from the property key name.
   * This helps generate more realistic fake data based on field names.
   */
  protected generateValueWithContext(
    schema: TSchema,
    keyName?: string,
  ): unknown {
    // If no key name context, use regular generation
    if (!keyName) {
      return this.generateValue(schema);
    }

    // Normalize key name: lowercase and remove underscores/hyphens
    // This allows matching "user_name", "user-name", "userName" etc.
    const normalizedKey = keyName.toLowerCase().replace(/[_-]/g, "");

    // Check if this is a string type that could benefit from context
    if (this.guard.isString(schema)) {
      const stringSchema = schema as TString;

      // Skip if it has a specific format already
      if (
        z.schema.format(stringSchema) &&
        z.schema.format(stringSchema) !== "string"
      ) {
        return this.generateValue(schema);
      }

      // Skip if it has enum values
      if ((stringSchema as any).enum) {
        return this.generateValue(schema);
      }

      // Generate based on common field name patterns
      if (normalizedKey.includes("email")) {
        return this.faker.internet.email();
      }
      if (normalizedKey.includes("firstname") || normalizedKey === "first") {
        return this.faker.person.firstName();
      }
      if (normalizedKey.includes("lastname") || normalizedKey === "last") {
        return this.faker.person.lastName();
      }
      if (normalizedKey.includes("fullname") || normalizedKey === "name") {
        return this.faker.person.fullName();
      }
      if (normalizedKey.includes("phone") || normalizedKey.includes("mobile")) {
        return this.faker.phone.number();
      }
      if (normalizedKey.includes("address")) {
        return this.faker.location.streetAddress();
      }
      if (normalizedKey.includes("city")) {
        return this.faker.location.city();
      }
      if (normalizedKey.includes("country")) {
        return this.faker.location.country();
      }
      if (
        normalizedKey.includes("state") ||
        normalizedKey.includes("province")
      ) {
        return this.faker.location.state();
      }
      if (normalizedKey.includes("zip") || normalizedKey.includes("postal")) {
        return this.faker.location.zipCode();
      }
      if (
        normalizedKey.includes("company") ||
        normalizedKey.includes("organization")
      ) {
        return this.faker.company.name();
      }
      if (
        normalizedKey.includes("job") ||
        normalizedKey.includes("title") ||
        normalizedKey.includes("position")
      ) {
        return this.faker.person.jobTitle();
      }
      if (normalizedKey.includes("username")) {
        return this.faker.internet.username();
      }
      if (normalizedKey.includes("url") || normalizedKey.includes("website")) {
        return this.faker.internet.url();
      }
      if (
        normalizedKey.includes("avatar") ||
        normalizedKey.includes("image") ||
        normalizedKey.includes("photo")
      ) {
        return this.faker.image.avatar();
      }
      if (normalizedKey.includes("color") || normalizedKey.includes("colour")) {
        return this.faker.color.human();
      }
      if (
        normalizedKey.includes("bio") ||
        normalizedKey.includes("about") ||
        normalizedKey.includes("description")
      ) {
        return this.faker.person.bio();
      }
    }

    // For number/integer fields, check for common patterns
    if (this.guard.isInteger(schema) || this.guard.isNumber(schema)) {
      if (normalizedKey.includes("age")) {
        return this.faker.number.int({ min: 18, max: 99 });
      }
      if (normalizedKey.includes("year")) {
        return this.faker.date.past().getFullYear();
      }
      if (normalizedKey.includes("month")) {
        return this.faker.number.int({ min: 1, max: 12 });
      }
      if (normalizedKey.includes("day")) {
        return this.faker.number.int({ min: 1, max: 31 });
      }
      if (
        normalizedKey.includes("price") ||
        normalizedKey.includes("amount") ||
        normalizedKey.includes("cost")
      ) {
        return this.faker.number.float({ min: 1, max: 1000, multipleOf: 0.01 });
      }
    }

    // Fallback to regular generation
    return this.generateValue(schema);
  }

  protected generateRecord(schema: TRecord): Record<string, unknown> {
    const record = schema as any;
    // zod stores the value schema on `.valueType` (def.valueType internally).
    const valueSchema =
      record.valueType ?? record._zod?.def?.valueType ?? z.any();

    // Generate 2-5 random key-value pairs
    const count = this.faker.number.int(this.options.defaultRecordEntries);
    const result: Record<string, unknown> = {};

    for (let i = 0; i < count; i++) {
      const key = this.faker.lorem.word();
      result[key] = this.generateValue(valueSchema);
    }

    return result;
  }
}
