/**
 * @since 4.0.0
 */
import * as Uuid from "uuid"
import type { Brand } from "../../Brand.ts"
import * as DateTime from "../../DateTime.ts"
import * as Effect from "../../Effect.ts"
import * as Option from "../../Option.ts"
import * as Predicate from "../../Predicate.ts"
import * as Schema from "../../Schema.ts"
import * as Getter from "../../SchemaGetter.ts"
import * as Transformation from "../../SchemaTransformation.ts"
import * as VariantSchema from "./VariantSchema.ts"

const {
  Class,
  Field,
  FieldExcept,
  FieldOnly,
  Struct,
  Union,
  extract,
  fieldEvolve
} = VariantSchema.make({
  variants: ["select", "insert", "update", "json", "jsonCreate", "jsonUpdate"],
  defaultVariant: "select"
})

/**
 * @since 4.0.0
 * @category models
 */
export type Any = Schema.Top & {
  readonly fields: Schema.Struct.Fields
  readonly insert: Schema.Top
  readonly update: Schema.Top
  readonly json: Schema.Top
  readonly jsonCreate: Schema.Top
  readonly jsonUpdate: Schema.Top
}

/**
 * @since 4.0.0
 * @category models
 */
export type VariantsDatabase = "select" | "insert" | "update"

/**
 * @since 4.0.0
 * @category models
 */
export type VariantsJson = "json" | "jsonCreate" | "jsonUpdate"

export {
  /**
   * A base class used for creating domain model schemas.
   *
   * It supports common variants for database and JSON apis.
   *
   * @since 4.0.0
   * @category constructors
   * @example
   * ```ts
   * import { Schema } from "effect"
   * import { Model } from "effect/unstable/schema"
   *
   * export const GroupId = Schema.Number.pipe(Schema.brand("GroupId"))
   *
   * export class Group extends Model.Class<Group>("Group")({
   *   id: Model.Generated(GroupId),
   *   name: Schema.String,
   *   createdAt: Model.DateTimeInsertFromDate,
   *   updatedAt: Model.DateTimeUpdateFromDate
   * }) {}
   *
   * // schema used for selects
   * Group
   *
   * // schema used for inserts
   * Group.insert
   *
   * // schema used for updates
   * Group.update
   *
   * // schema used for json api
   * Group.json
   * Group.jsonCreate
   * Group.jsonUpdate
   *
   * // you can also turn them into classes
   * class GroupJson extends Schema.Class<GroupJson>("GroupJson")(Group.json) {
   *   get upperName() {
   *     return this.name.toUpperCase()
   *   }
   * }
   * ```
   */
  Class,
  /**
   * @since 4.0.0
   * @category extraction
   */
  extract,
  /**
   * @since 4.0.0
   * @category fields
   */
  Field,
  /**
   * @since 4.0.0
   * @category fields
   */
  fieldEvolve,
  /**
   * @since 4.0.0
   * @category fields
   */
  FieldExcept,
  /**
   * @since 4.0.0
   * @category fields
   */
  FieldOnly,
  /**
   * @since 4.0.0
   * @category constructors
   */
  Struct,
  /**
   * @since 4.0.0
   * @category constructors
   */
  Union
}

/**
 * @since 4.0.0
 * @category fields
 */
export const fields: <A extends VariantSchema.Struct<any>>(self: A) => A[typeof VariantSchema.TypeId] =
  VariantSchema.fields

/**
 * @since 4.0.0
 * @category overrideable
 */
export const Override: <A>(value: A) => A & Brand<"Override"> = VariantSchema.Override

/**
 * @since 4.0.0
 * @category generated
 */
export interface Generated<S extends Schema.Top> extends
  VariantSchema.Field<{
    readonly select: S
    readonly update: S
    readonly json: S
  }>
{}

/**
 * A field that represents a column that is generated by the database.
 *
 * It is available for selection and update, but not for insertion.
 *
 * @since 4.0.0
 * @category generated
 */
export const Generated = <S extends Schema.Top>(
  schema: S
): Generated<S> =>
  Field({
    select: schema,
    update: schema,
    json: schema
  })

/**
 * @since 4.0.0
 * @category generated
 */
export interface GeneratedByApp<S extends Schema.Top> extends
  VariantSchema.Field<{
    readonly select: S
    readonly insert: S
    readonly update: S
    readonly json: S
  }>
{}

/**
 * A field that represents a column that is generated by the application.
 *
 * It is required by the database, but not by the JSON variants.
 *
 * @since 4.0.0
 * @category generated
 */
export const GeneratedByApp = <S extends Schema.Top>(schema: S): GeneratedByApp<S> =>
  Field({
    select: schema,
    insert: schema,
    update: schema,
    json: schema
  })

/**
 * @since 4.0.0
 * @category sensitive
 */
export interface Sensitive<S extends Schema.Top> extends
  VariantSchema.Field<{
    readonly select: S
    readonly insert: S
    readonly update: S
  }>
{}

/**
 * A field that represents a sensitive value that should not be exposed in the
 * JSON variants.
 *
 * @since 4.0.0
 * @category sensitive
 */
export const Sensitive = <S extends Schema.Top>(schema: S): Sensitive<S> =>
  Field({
    select: schema,
    insert: schema,
    update: schema
  })

/**
 * @since 4.0.0
 * @category optional
 */
export interface optionalOption<S extends Schema.Top>
  extends Schema.decodeTo<Schema.Option<Schema.toType<S>>, Schema.optionalKey<Schema.NullOr<S>>>
{}

/**
 * @since 4.0.0
 * @category optional
 */
export const optionalOption = <S extends Schema.Top>(schema: S): optionalOption<S> =>
  Schema.optionalKey(Schema.NullOr(schema)).pipe(
    Schema.decodeTo(
      Schema.Option(Schema.toType(schema)),
      Transformation.transformOptional<Option.Option<S["Type"]>, S["Type"] | null>({
        decode: (oe) => oe.pipe(Option.filter(Predicate.isNotNull), Option.some),
        encode: Option.flatten
      }) as any
    )
  )

/**
 * Convert a field to one that is optional for all variants.
 *
 * For the database variants, it will accept `null`able values.
 * For the JSON variants, it will also accept missing keys.
 *
 * @since 4.0.0
 * @category optional
 */
export interface FieldOption<S extends Schema.Top> extends
  VariantSchema.Field<{
    readonly select: Schema.OptionFromNullOr<S>
    readonly insert: Schema.OptionFromNullOr<S>
    readonly update: Schema.OptionFromNullOr<S>
    readonly json: optionalOption<S>
    readonly jsonCreate: optionalOption<S>
    readonly jsonUpdate: optionalOption<S>
  }>
{}

/**
 * Convert a field to one that is optional for all variants.
 *
 * For the database variants, it will accept `null`able values.
 * For the JSON variants, it will also accept missing keys.
 *
 * @since 4.0.0
 * @category optional
 */
export const FieldOption: <Field extends VariantSchema.Field<any> | Schema.Top>(
  self: Field
) => Field extends Schema.Top ? FieldOption<Field>
  : Field extends VariantSchema.Field<infer S> ? VariantSchema.Field<
      {
        readonly [K in keyof S]: S[K] extends Schema.Top ? K extends VariantsDatabase ? Schema.OptionFromNullOr<S[K]> :
          optionalOption<S[K]>
          : never
      }
    > :
  never = fieldEvolve({
    select: Schema.OptionFromNullOr,
    insert: Schema.OptionFromNullOr,
    update: Schema.OptionFromNullOr,
    json: optionalOption,
    jsonCreate: optionalOption,
    jsonUpdate: optionalOption
  }) as any

/**
 * @since 4.0.0
 * @category booleans
 */
export interface BooleanSqlite extends
  VariantSchema.Field<{
    readonly select: Schema.BooleanFromBit
    readonly insert: Schema.BooleanFromBit
    readonly update: Schema.BooleanFromBit
    readonly json: Schema.Boolean
    readonly jsonCreate: Schema.Boolean
    readonly jsonUpdate: Schema.Boolean
  }>
{}

/**
 * A schema for sqlite booleans that are represented as `0 | 1` in database
 * variants and `boolean` in JSON variants.
 *
 * @since 4.0.0
 * @category booleans
 */
export const BooleanSqlite: BooleanSqlite = Field({
  select: Schema.BooleanFromBit,
  insert: Schema.BooleanFromBit,
  update: Schema.BooleanFromBit,
  json: Schema.Boolean,
  jsonCreate: Schema.Boolean,
  jsonUpdate: Schema.Boolean
})

/**
 * @since 4.0.0
 * @category date & time
 */
export interface Date extends Schema.decodeTo<Schema.instanceOf<DateTime.Utc>, Schema.String> {}

/**
 * A schema for a `DateTime.Utc` that is serialized as a date string in the
 * format `YYYY-MM-DD`.
 *
 * @since 4.0.0
 * @category date & time
 */
export const Date: Date = Schema.String.pipe(
  Schema.decodeTo(Schema.DateTimeUtc, {
    decode: Getter.dateTimeUtcFromInput().map(DateTime.removeTime),
    encode: Getter.transform(DateTime.formatIsoDate)
  })
)

/**
 * @since 4.0.0
 * @category date & time
 */
export const DateWithNow = VariantSchema.Overrideable(Date, {
  defaultValue: Effect.map(DateTime.now, DateTime.removeTime)
})

/**
 * @since 4.0.0
 * @category date & time
 */
export const DateTimeWithNow = VariantSchema.Overrideable(Schema.DateTimeUtcFromString, {
  defaultValue: DateTime.now
})

/**
 * @since 4.0.0
 * @category date & time
 */
export const DateTimeFromDateWithNow = VariantSchema.Overrideable(Schema.DateTimeUtcFromDate, {
  defaultValue: DateTime.now
})

/**
 * @since 4.0.0
 * @category date & time
 */
export const DateTimeFromNumberWithNow = VariantSchema.Overrideable(Schema.DateTimeUtcFromMillis, {
  defaultValue: DateTime.now
})

/**
 * @since 4.0.0
 * @category date & time
 */
export interface DateTimeInsert extends
  VariantSchema.Field<{
    readonly select: Schema.DateTimeUtcFromString
    readonly insert: VariantSchema.Overrideable<Schema.DateTimeUtcFromString>
    readonly json: Schema.DateTimeUtcFromString
  }>
{}

/**
 * A field that represents a date-time value that is inserted as the current
 * `DateTime.Utc`. It is serialized as a string for the database.
 *
 * It is omitted from updates and is available for selection.
 *
 * @since 4.0.0
 * @category date & time
 */
export const DateTimeInsert: DateTimeInsert = Field({
  select: Schema.DateTimeUtcFromString,
  insert: DateTimeWithNow,
  json: Schema.DateTimeUtcFromString
})

/**
 * @since 4.0.0
 * @category date & time
 */
export interface DateTimeInsertFromDate extends
  VariantSchema.Field<{
    readonly select: Schema.DateTimeUtcFromDate
    readonly insert: VariantSchema.Overrideable<Schema.DateTimeUtcFromDate>
    readonly json: Schema.DateTimeUtcFromString
  }>
{}

/**
 * A field that represents a date-time value that is inserted as the current
 * `DateTime.Utc`. It is serialized as a `Date` for the database.
 *
 * It is omitted from updates and is available for selection.
 *
 * @since 4.0.0
 * @category date & time
 */
export const DateTimeInsertFromDate: DateTimeInsertFromDate = Field({
  select: Schema.DateTimeUtcFromDate,
  insert: DateTimeFromDateWithNow,
  json: Schema.DateTimeUtcFromString
})

/**
 * @since 4.0.0
 * @category date & time
 */
export interface DateTimeInsertFromNumber extends
  VariantSchema.Field<{
    readonly select: Schema.DateTimeUtcFromMillis
    readonly insert: VariantSchema.Overrideable<Schema.DateTimeUtcFromMillis>
    readonly json: Schema.DateTimeUtcFromMillis
  }>
{}

/**
 * A field that represents a date-time value that is inserted as the current
 * `DateTime.Utc`. It is serialized as a `number`.
 *
 * It is omitted from updates and is available for selection.
 *
 * @since 4.0.0
 * @category date & time
 */
export const DateTimeInsertFromNumber: DateTimeInsertFromNumber = Field({
  select: Schema.DateTimeUtcFromMillis,
  insert: DateTimeFromNumberWithNow,
  json: Schema.DateTimeUtcFromMillis
})

/**
 * @since 4.0.0
 * @category date & time
 */
export interface DateTimeUpdate extends
  VariantSchema.Field<{
    readonly select: Schema.DateTimeUtcFromString
    readonly insert: VariantSchema.Overrideable<Schema.DateTimeUtcFromString>
    readonly update: VariantSchema.Overrideable<Schema.DateTimeUtcFromString>
    readonly json: Schema.DateTimeUtcFromString
  }>
{}

/**
 * A field that represents a date-time value that is updated as the current
 * `DateTime.Utc`. It is serialized as a string for the database.
 *
 * It is set to the current `DateTime.Utc` on updates and inserts and is
 * available for selection.
 *
 * @since 4.0.0
 * @category date & time
 */
export const DateTimeUpdate: DateTimeUpdate = Field({
  select: Schema.DateTimeUtcFromString,
  insert: DateTimeWithNow,
  update: DateTimeWithNow,
  json: Schema.DateTimeUtcFromString
})

/**
 * @since 4.0.0
 * @category date & time
 */
export interface DateTimeUpdateFromDate extends
  VariantSchema.Field<{
    readonly select: Schema.DateTimeUtcFromDate
    readonly insert: VariantSchema.Overrideable<Schema.DateTimeUtcFromDate>
    readonly update: VariantSchema.Overrideable<Schema.DateTimeUtcFromDate>
    readonly json: Schema.DateTimeUtcFromString
  }>
{}

/**
 * A field that represents a date-time value that is updated as the current
 * `DateTime.Utc`. It is serialized as a `Date` for the database.
 *
 * It is set to the current `DateTime.Utc` on updates and inserts and is
 * available for selection.
 *
 * @since 4.0.0
 * @category date & time
 */
export const DateTimeUpdateFromDate: DateTimeUpdateFromDate = Field({
  select: Schema.DateTimeUtcFromDate,
  insert: DateTimeFromDateWithNow,
  update: DateTimeFromDateWithNow,
  json: Schema.DateTimeUtcFromString
})

/**
 * @since 4.0.0
 * @category date & time
 */
export interface DateTimeUpdateFromNumber extends
  VariantSchema.Field<{
    readonly select: Schema.DateTimeUtcFromMillis
    readonly insert: VariantSchema.Overrideable<Schema.DateTimeUtcFromMillis>
    readonly update: VariantSchema.Overrideable<Schema.DateTimeUtcFromMillis>
    readonly json: Schema.DateTimeUtcFromMillis
  }>
{}

/**
 * A field that represents a date-time value that is updated as the current
 * `DateTime.Utc`. It is serialized as a `number`.
 *
 * It is set to the current `DateTime.Utc` on updates and inserts and is
 * available for selection.
 *
 * @since 4.0.0
 * @category date & time
 */
export const DateTimeUpdateFromNumber: DateTimeUpdateFromNumber = Field({
  select: Schema.DateTimeUtcFromMillis,
  insert: DateTimeFromNumberWithNow,
  update: DateTimeFromNumberWithNow,
  json: Schema.DateTimeUtcFromMillis
})

/**
 * @since 4.0.0
 * @category json
 */
export interface JsonFromString<S extends Schema.Top> extends
  VariantSchema.Field<{
    readonly select: Schema.fromJsonString<S>
    readonly insert: Schema.fromJsonString<S>
    readonly update: Schema.fromJsonString<S>
    readonly json: S
    readonly jsonCreate: S
    readonly jsonUpdate: S
  }>
{}

/**
 * A field that represents a JSON value stored as text in the database.
 *
 * The "json" variants will use the object schema directly.
 *
 * @since 4.0.0
 * @category json
 */
export const JsonFromString = <S extends Schema.Top>(
  schema: S
): JsonFromString<S> => {
  const parsed = Schema.fromJsonString(Schema.toCodecJson(schema)) as any
  return Field({
    select: parsed,
    insert: parsed,
    update: parsed,
    json: schema,
    jsonCreate: schema,
    jsonUpdate: schema
  })
}

/**
 * @since 4.0.0
 * @category uuid
 */
export interface UuidV4Insert<B extends string> extends
  VariantSchema.Field<{
    readonly select: Schema.brand<Schema.instanceOf<Uint8Array<ArrayBuffer>>, B>
    readonly insert: Schema.withConstructorDefault<Schema.brand<Schema.instanceOf<Uint8Array<ArrayBuffer>>, B>>
    readonly update: Schema.brand<Schema.instanceOf<Uint8Array<ArrayBuffer>>, B>
    readonly json: Schema.brand<Schema.instanceOf<Uint8Array<ArrayBuffer>>, B>
  }>
{}

/**
 * @since 4.0.0
 * @category Uint8Array
 */
export const Uint8Array: Schema.instanceOf<Uint8Array<ArrayBuffer>> = Schema.Uint8Array as Schema.instanceOf<
  globalThis.Uint8Array<ArrayBuffer>
>

/**
 * @since 4.0.0
 * @category uuid
 */
export const UuidV4WithGenerate = <B extends string>(
  schema: Schema.brand<Schema.instanceOf<Uint8Array<ArrayBuffer>>, B>
): Schema.withConstructorDefault<Schema.brand<Schema.instanceOf<Uint8Array<ArrayBuffer>>, B>> =>
  schema.pipe(Schema.withConstructorDefault(Effect.sync(() => Uuid.v4({}, new globalThis.Uint8Array(16)))))

/**
 * A field that represents a binary UUID v4 that is generated on inserts.
 *
 * @since 4.0.0
 * @category uuid
 */
export const UuidV4Insert = <const B extends string>(
  schema: Schema.brand<Schema.instanceOf<Uint8Array<ArrayBuffer>>, B>
): UuidV4Insert<B> =>
  Field({
    select: schema,
    insert: UuidV4WithGenerate(schema),
    update: schema,
    json: schema
  })
