// Credits to @bnjmnt4n (https://www.npmjs.com/package/postgrest-query)

import { GenericSchema, Prettify } from './types'

type Whitespace = ' ' | '\n' | '\t'

type LowerAlphabet =
  | 'a'
  | 'b'
  | 'c'
  | 'd'
  | 'e'
  | 'f'
  | 'g'
  | 'h'
  | 'i'
  | 'j'
  | 'k'
  | 'l'
  | 'm'
  | 'n'
  | 'o'
  | 'p'
  | 'q'
  | 'r'
  | 's'
  | 't'
  | 'u'
  | 'v'
  | 'w'
  | 'x'
  | 'y'
  | 'z'

type Alphabet = LowerAlphabet | Uppercase<LowerAlphabet>

type Digit = '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '0'

type Letter = Alphabet | Digit | '_'

type Json = string | number | boolean | null | { [key: string]: Json } | Json[]

// /**
//  * Parsed node types.
//  * Currently only `*` and all other fields.
//  */
// type ParsedNode =
//   | { star: true }
//   | { name: string; original: string }
//   | { name: string; foreignTable: true }
//   | { name: string; type: T };

/**
 * Parser errors.
 */
type ParserError<Message extends string> = { error: true } & Message
type GenericStringError = ParserError<'Received a generic string'>
export type SelectQueryError<Message extends string> = { error: true } & Message

/**
 * Trims whitespace from the left of the input.
 */
type EatWhitespace<Input extends string> = string extends Input
  ? GenericStringError
  : Input extends `${Whitespace}${infer Remainder}`
  ? EatWhitespace<Remainder>
  : Input

type HasFKey<FKeyName, Relationships> = Relationships extends [infer R]
  ? R extends { foreignKeyName: FKeyName }
    ? true
    : false
  : Relationships extends [infer R, ...infer Rest]
  ? HasFKey<FKeyName, [R]> extends true
    ? true
    : HasFKey<FKeyName, Rest>
  : false

type HasFKeyToFRel<FRelName, Relationships> = Relationships extends [infer R]
  ? R extends { referencedRelation: FRelName }
    ? true
    : false
  : Relationships extends [infer R, ...infer Rest]
  ? HasFKeyToFRel<FRelName, [R]> extends true
    ? true
    : HasFKeyToFRel<FRelName, Rest>
  : false

/**
 * Constructs a type definition for a single field of an object.
 *
 * @param Definitions Record of definitions, possibly generated from PostgREST's OpenAPI spec.
 * @param Name Name of the table being queried.
 * @param Field Single field parsed by `ParseQuery`.
 */
type ConstructFieldDefinition<
  Schema extends GenericSchema,
  Row extends Record<string, unknown>,
  Relationships,
  Field
> = Field extends { star: true }
  ? Row
  : Field extends { name: string; original: string; hint: string; children: unknown[] }
  ? {
      [_ in Field['name']]: GetResultHelper<
        Schema,
        (Schema['Tables'] & Schema['Views'])[Field['original']]['Row'],
        (Schema['Tables'] & Schema['Views'])[Field['original']] extends { Relationships: infer R }
          ? R
          : unknown,
        Field['children'],
        unknown
      > extends infer Child
        ? Relationships extends unknown[]
          ? HasFKey<Field['hint'], Relationships> extends true
            ? Child | null
            : Child[]
          : Child[]
        : never
    }
  : Field extends { name: string; original: string; children: unknown[] }
  ? {
      [_ in Field['name']]: GetResultHelper<
        Schema,
        (Schema['Tables'] & Schema['Views'])[Field['original']]['Row'],
        (Schema['Tables'] & Schema['Views'])[Field['original']] extends { Relationships: infer R }
          ? R
          : unknown,
        Field['children'],
        unknown
      > extends infer Child
        ? Relationships extends unknown[]
          ? HasFKeyToFRel<Field['original'], Relationships> extends true
            ? Child | null
            : Child[]
          : Child[]
        : never
    }
  : Field extends { name: string; original: string }
  ? Field['original'] extends keyof Row
    ? { [K in Field['name']]: Row[Field['original']] }
    : SelectQueryError<`Referencing missing column \`${Field['original']}\``>
  : Field extends { name: string; type: infer T }
  ? { [K in Field['name']]: T }
  : Record<string, unknown>

/**
 * Notes: all `Parse*` types assume that their input strings have their whitespace
 * removed. They return tuples of ["Return Value", "Remainder of text"] or
 * a `ParserError`.
 */

/**
 * Reads a consecutive sequence of more than 1 letter,
 * where letters are `[0-9a-zA-Z_]`.
 */
type ReadLetters<Input extends string> = string extends Input
  ? GenericStringError
  : ReadLettersHelper<Input, ''> extends [`${infer Letters}`, `${infer Remainder}`]
  ? Letters extends ''
    ? ParserError<`Expected letter at \`${Input}\``>
    : [Letters, Remainder]
  : ReadLettersHelper<Input, ''>

type ReadLettersHelper<Input extends string, Acc extends string> = string extends Input
  ? GenericStringError
  : Input extends `${infer L}${infer Remainder}`
  ? L extends Letter
    ? ReadLettersHelper<Remainder, `${Acc}${L}`>
    : [Acc, Input]
  : [Acc, '']

/**
 * Reads a consecutive sequence of more than 1 double-quoted letters,
 * where letters are `[^"]`.
 */
type ReadQuotedLetters<Input extends string> = string extends Input
  ? GenericStringError
  : Input extends `"${infer Remainder}`
  ? ReadQuotedLettersHelper<Remainder, ''> extends [`${infer Letters}`, `${infer Remainder}`]
    ? Letters extends ''
      ? ParserError<`Expected string at \`${Remainder}\``>
      : [Letters, Remainder]
    : ReadQuotedLettersHelper<Remainder, ''>
  : ParserError<`Not a double-quoted string at \`${Input}\``>

type ReadQuotedLettersHelper<Input extends string, Acc extends string> = string extends Input
  ? GenericStringError
  : Input extends `${infer L}${infer Remainder}`
  ? L extends '"'
    ? [Acc, Remainder]
    : ReadQuotedLettersHelper<Remainder, `${Acc}${L}`>
  : ParserError<`Missing closing double-quote in \`"${Acc}${Input}\``>

/**
 * Parses a (possibly double-quoted) identifier.
 * For now, identifiers are just sequences of more than 1 letter.
 */
type ParseIdentifier<Input extends string> = ReadLetters<Input> extends [
  infer Name,
  `${infer Remainder}`
]
  ? [Name, `${Remainder}`]
  : ReadQuotedLetters<Input> extends [infer Name, `${infer Remainder}`]
  ? [Name, `${Remainder}`]
  : ParserError<`No (possibly double-quoted) identifier at \`${Input}\``>

/**
 * Parses a node.
 * A node is one of the following:
 * - `*`
 * - `field`
 * - `field->json...`
 * - `field(nodes)`
 * - `field!hint(nodes)`
 * - `field!inner(nodes)`
 * - `field!hint!inner(nodes)`
 * - `renamed_field:field`
 * - `renamed_field:field->json...`
 * - `renamed_field:field(nodes)`
 * - `renamed_field:field!hint(nodes)`
 * - `renamed_field:field!inner(nodes)`
 * - `renamed_field:field!hint!inner(nodes)`
 *
 * TODO: casting operators `::text`, more support for JSON operators `->`, `->>`.
 */
type ParseNode<Input extends string> = Input extends ''
  ? ParserError<'Empty string'>
  : // `*`
  Input extends `*${infer Remainder}`
  ? [{ star: true }, EatWhitespace<Remainder>]
  : ParseIdentifier<Input> extends [infer Name, `${infer Remainder}`]
  ? EatWhitespace<Remainder> extends `!inner${infer Remainder}`
    ? ParseEmbeddedResource<EatWhitespace<Remainder>> extends [infer Fields, `${infer Remainder}`]
      ? // `field!inner(nodes)`
        [{ name: Name; original: Name; children: Fields }, EatWhitespace<Remainder>]
      : ParseEmbeddedResource<EatWhitespace<Remainder>> extends ParserError<string>
      ? ParseEmbeddedResource<EatWhitespace<Remainder>>
      : ParserError<'Expected embedded resource after `!inner`'>
    : EatWhitespace<Remainder> extends `!${infer Remainder}`
    ? ParseIdentifier<EatWhitespace<Remainder>> extends [infer Hint, `${infer Remainder}`]
      ? EatWhitespace<Remainder> extends `!inner${infer Remainder}`
        ? ParseEmbeddedResource<EatWhitespace<Remainder>> extends [
            infer Fields,
            `${infer Remainder}`
          ]
          ? // `field!hint!inner(nodes)`
            [{ name: Name; original: Name; hint: Hint; children: Fields }, EatWhitespace<Remainder>]
          : ParseEmbeddedResource<EatWhitespace<Remainder>> extends ParserError<string>
          ? ParseEmbeddedResource<EatWhitespace<Remainder>>
          : ParserError<'Expected embedded resource after `!inner`'>
        : ParseEmbeddedResource<EatWhitespace<Remainder>> extends [
            infer Fields,
            `${infer Remainder}`
          ]
        ? // `field!hint(nodes)`
          [{ name: Name; original: Name; hint: Hint; children: Fields }, EatWhitespace<Remainder>]
        : ParseEmbeddedResource<EatWhitespace<Remainder>> extends ParserError<string>
        ? ParseEmbeddedResource<EatWhitespace<Remainder>>
        : ParserError<'Expected embedded resource after `!hint`'>
      : ParserError<'Expected identifier after `!`'>
    : EatWhitespace<Remainder> extends `:${infer Remainder}`
    ? ParseIdentifier<EatWhitespace<Remainder>> extends [infer OriginalName, `${infer Remainder}`]
      ? EatWhitespace<Remainder> extends `!inner${infer Remainder}`
        ? ParseEmbeddedResource<EatWhitespace<Remainder>> extends [
            infer Fields,
            `${infer Remainder}`
          ]
          ? // `renamed_field:field!inner(nodes)`
            [{ name: Name; original: OriginalName; children: Fields }, EatWhitespace<Remainder>]
          : ParseEmbeddedResource<EatWhitespace<Remainder>> extends ParserError<string>
          ? ParseEmbeddedResource<EatWhitespace<Remainder>>
          : ParserError<'Expected embedded resource after `!inner`'>
        : EatWhitespace<Remainder> extends `!${infer Remainder}`
        ? ParseIdentifier<EatWhitespace<Remainder>> extends [infer Hint, `${infer Remainder}`]
          ? EatWhitespace<Remainder> extends `!inner${infer Remainder}`
            ? ParseEmbeddedResource<EatWhitespace<Remainder>> extends [
                infer Fields,
                `${infer Remainder}`
              ]
              ? // `renamed_field:field!hint!inner(nodes)`
                [
                  { name: Name; original: OriginalName; hint: Hint; children: Fields },
                  EatWhitespace<Remainder>
                ]
              : ParseEmbeddedResource<EatWhitespace<Remainder>> extends ParserError<string>
              ? ParseEmbeddedResource<EatWhitespace<Remainder>>
              : ParserError<'Expected embedded resource after `!inner`'>
            : ParseEmbeddedResource<EatWhitespace<Remainder>> extends [
                infer Fields,
                `${infer Remainder}`
              ]
            ? // `renamed_field:field!hint(nodes)`
              [
                {
                  name: Name
                  original: OriginalName
                  hint: Hint
                  children: Fields
                },
                EatWhitespace<Remainder>
              ]
            : ParseEmbeddedResource<EatWhitespace<Remainder>> extends ParserError<string>
            ? ParseEmbeddedResource<EatWhitespace<Remainder>>
            : ParserError<'Expected embedded resource after `!hint`'>
          : ParserError<'Expected identifier after `!`'>
        : ParseEmbeddedResource<EatWhitespace<Remainder>> extends [
            infer Fields,
            `${infer Remainder}`
          ]
        ? // `renamed_field:field(nodes)`
          [{ name: Name; original: OriginalName; children: Fields }, EatWhitespace<Remainder>]
        : ParseJsonAccessor<EatWhitespace<Remainder>> extends [
            infer _PropertyName,
            infer PropertyType,
            `${infer Remainder}`
          ]
        ? // `renamed_field:field->json...`
          [{ name: Name; type: PropertyType }, EatWhitespace<Remainder>]
        : ParseEmbeddedResource<EatWhitespace<Remainder>> extends ParserError<string>
        ? ParseEmbeddedResource<EatWhitespace<Remainder>>
        : // `renamed_field:field`
          [{ name: Name; original: OriginalName }, EatWhitespace<Remainder>]
      : ParseIdentifier<EatWhitespace<Remainder>>
    : ParseEmbeddedResource<EatWhitespace<Remainder>> extends [infer Fields, `${infer Remainder}`]
    ? // `field(nodes)`
      [{ name: Name; original: Name; children: Fields }, EatWhitespace<Remainder>]
    : ParseJsonAccessor<EatWhitespace<Remainder>> extends [
        infer PropertyName,
        infer PropertyType,
        `${infer Remainder}`
      ]
    ? // `field->json...`
      [{ name: PropertyName; type: PropertyType }, EatWhitespace<Remainder>]
    : ParseEmbeddedResource<EatWhitespace<Remainder>> extends ParserError<string>
    ? ParseEmbeddedResource<EatWhitespace<Remainder>>
    : // `field`
      [{ name: Name; original: Name }, EatWhitespace<Remainder>]
  : ParserError<`Expected identifier at \`${Input}\``>

/**
 * Parses a JSON property accessor of the shape `->a->b->c`. The last accessor in
 * the series may convert to text by using the ->> operator instead of ->.
 *
 * Returns a tuple of ["Last property name", "Last property type", "Remainder of text"]
 * or the original string input indicating that no opening `->` was found.
 */
type ParseJsonAccessor<Input extends string> = Input extends `->${infer Remainder}`
  ? Remainder extends `>${infer Remainder}`
    ? ParseIdentifier<Remainder> extends [infer Name, `${infer Remainder}`]
      ? [Name, string, EatWhitespace<Remainder>]
      : ParserError<'Expected property name after `->>`'>
    : ParseIdentifier<Remainder> extends [infer Name, `${infer Remainder}`]
    ? ParseJsonAccessor<Remainder> extends [
        infer PropertyName,
        infer PropertyType,
        `${infer Remainder}`
      ]
      ? [PropertyName, PropertyType, EatWhitespace<Remainder>]
      : [Name, Json, EatWhitespace<Remainder>]
    : ParserError<'Expected property name after `->`'>
  : Input

/**
 * Parses an embedded resource, which is an opening `(`, followed by a sequence of
 * nodes, separated by `,`, then a closing `)`.
 *
 * Returns a tuple of ["Parsed fields", "Remainder of text"], an error,
 * or the original string input indicating that no opening `(` was found.
 */
type ParseEmbeddedResource<Input extends string> = Input extends `(${infer Remainder}`
  ? ParseNodes<EatWhitespace<Remainder>> extends [infer Fields, `${infer Remainder}`]
    ? EatWhitespace<Remainder> extends `)${infer Remainder}`
      ? Fields extends []
        ? ParserError<'Expected fields after `(`'>
        : [Fields, EatWhitespace<Remainder>]
      : ParserError<`Expected ")"`>
    : ParseNodes<EatWhitespace<Remainder>>
  : Input

/**
 * Parses a sequence of nodes, separated by `,`.
 *
 * Returns a tuple of ["Parsed fields", "Remainder of text"] or an error.
 */
type ParseNodes<Input extends string> = string extends Input
  ? GenericStringError
  : ParseNodesHelper<Input, []>

type ParseNodesHelper<Input extends string, Fields extends unknown[]> = ParseNode<Input> extends [
  infer Field,
  `${infer Remainder}`
]
  ? EatWhitespace<Remainder> extends `,${infer Remainder}`
    ? ParseNodesHelper<EatWhitespace<Remainder>, [Field, ...Fields]>
    : [[Field, ...Fields], EatWhitespace<Remainder>]
  : ParseNode<Input>

/**
 * Parses a query.
 * A query is a sequence of nodes, separated by `,`, ensuring that there is
 * no remaining input after all nodes have been parsed.
 *
 * Returns an array of parsed nodes, or an error.
 */
type ParseQuery<Query extends string> = string extends Query
  ? GenericStringError
  : ParseNodes<EatWhitespace<Query>> extends [infer Fields, `${infer Remainder}`]
  ? EatWhitespace<Remainder> extends ''
    ? Fields
    : ParserError<`Unexpected input: ${Remainder}`>
  : ParseNodes<EatWhitespace<Query>>

type GetResultHelper<
  Schema extends GenericSchema,
  Row extends Record<string, unknown>,
  Relationships,
  Fields extends unknown[],
  Acc
> = Fields extends [infer R]
  ? ConstructFieldDefinition<Schema, Row, Relationships, R> extends SelectQueryError<infer E>
    ? SelectQueryError<E>
    : GetResultHelper<
        Schema,
        Row,
        Relationships,
        [],
        ConstructFieldDefinition<Schema, Row, Relationships, R> & Acc
      >
  : Fields extends [infer R, ...infer Rest]
  ? ConstructFieldDefinition<Schema, Row, Relationships, R> extends SelectQueryError<infer E>
    ? SelectQueryError<E>
    : GetResultHelper<
        Schema,
        Row,
        Relationships,
        Rest,
        ConstructFieldDefinition<Schema, Row, Relationships, R> & Acc
      >
  : Prettify<Acc>

/**
 * Constructs a type definition for an object based on a given PostgREST query.
 *
 * @param Row Record<string, unknown>.
 * @param Query Select query string literal to parse.
 */
export type GetResult<
  Schema extends GenericSchema,
  Row extends Record<string, unknown>,
  Relationships,
  Query extends string
> = ParseQuery<Query> extends unknown[]
  ? GetResultHelper<Schema, Row, Relationships, ParseQuery<Query>, unknown>
  : ParseQuery<Query>
