// Credits to @bnjmnt4n (https://www.npmjs.com/package/postgrest-query)
// See https://github.com/PostgREST/postgrest/blob/2f91853cb1de18944a4556df09e52450b881cfb3/src/PostgREST/ApiRequest/QueryParams.hs#L282-L284

import { SimplifyDeep } from '../types/types'
import { JsonPathToAccessor } from './utils'

/**
 * 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.
 */
export type ParseQuery<Query extends string> = string extends Query
  ? GenericStringError
  : ParseNodes<EatWhitespace<Query>> extends [infer Nodes, `${infer Remainder}`]
    ? Nodes extends Ast.Node[]
      ? EatWhitespace<Remainder> extends ''
        ? SimplifyDeep<Nodes>
        : ParserError<`Unexpected input: ${Remainder}`>
      : ParserError<'Invalid nodes array structure'>
    : ParseNodes<EatWhitespace<Query>>

/**
 * 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`.
 */

/**
 * 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, Nodes extends Ast.Node[]> =
  ParseNode<Input> extends [infer Node, `${infer Remainder}`]
    ? Node extends Ast.Node
      ? EatWhitespace<Remainder> extends `,${infer Remainder}`
        ? ParseNodesHelper<EatWhitespace<Remainder>, [...Nodes, Node]>
        : [[...Nodes, Node], EatWhitespace<Remainder>]
      : ParserError<'Invalid node type in nodes helper'>
    : ParseNode<Input>
/**
 * Parses a node.
 * A node is one of the following:
 * - `*`
 * - a field, as defined above
 * - a renamed field, `renamed_field:field`
 * - a spread field, `...field`
 */
type ParseNode<Input extends string> = Input extends ''
  ? ParserError<'Empty string'>
  : // `*`
    Input extends `*${infer Remainder}`
    ? [Ast.StarNode, EatWhitespace<Remainder>]
    : // `...field`
      Input extends `...${infer Remainder}`
      ? ParseField<EatWhitespace<Remainder>> extends [infer TargetField, `${infer Remainder}`]
        ? TargetField extends Ast.FieldNode
          ? [{ type: 'spread'; target: TargetField }, EatWhitespace<Remainder>]
          : ParserError<'Invalid target field type in spread'>
        : ParserError<`Unable to parse spread resource at \`${Input}\``>
      : ParseIdentifier<Input> extends [infer NameOrAlias, `${infer Remainder}`]
        ? EatWhitespace<Remainder> extends `::${infer _}`
          ? // It's a type cast and not an alias, so treat it as part of the field.
            ParseField<Input>
          : EatWhitespace<Remainder> extends `:${infer Remainder}`
            ? // `alias:`
              ParseField<EatWhitespace<Remainder>> extends [infer Field, `${infer Remainder}`]
              ? Field extends Ast.FieldNode
                ? [Omit<Field, 'alias'> & { alias: NameOrAlias }, EatWhitespace<Remainder>]
                : ParserError<'Invalid field type in alias parsing'>
              : ParserError<`Unable to parse renamed field at \`${Input}\``>
            : // Otherwise, just parse it as a field without alias.
              ParseField<Input>
        : ParserError<`Expected identifier at \`${Input}\``>

/**
 * Parses a field without preceding alias.
 * A field is one of the following:
 * - a top-level `count` field: https://docs.postgrest.org/en/v12/references/api/aggregate_functions.html#the-case-of-count
 * - a field with an embedded resource
 *   - `field(nodes)`
 *   - `field!hint(nodes)`
 *   - `field!inner(nodes)`
 *   - `field!left(nodes)`
 *   - `field!hint!inner(nodes)`
 *   - `field!hint!left(nodes)`
 * - a field without an embedded resource (see {@link ParseNonEmbeddedResourceField})
 */
type ParseField<Input extends string> = Input extends ''
  ? ParserError<'Empty string'>
  : ParseIdentifier<Input> extends [infer Name, `${infer Remainder}`]
    ? Name extends 'count'
      ? ParseCountField<Input>
      : Remainder extends `!inner${infer Remainder}`
        ? ParseEmbeddedResource<EatWhitespace<Remainder>> extends [
            infer Children,
            `${infer Remainder}`,
          ]
          ? Children extends Ast.Node[]
            ? // `field!inner(nodes)`
              [{ type: 'field'; name: Name; innerJoin: true; children: Children }, Remainder]
            : ParserError<'Invalid children array in inner join'>
          : CreateParserErrorIfRequired<
              ParseEmbeddedResource<EatWhitespace<Remainder>>,
              `Expected embedded resource after "!inner" at \`${Remainder}\``
            >
        : EatWhitespace<Remainder> extends `!left${infer Remainder}`
          ? ParseEmbeddedResource<EatWhitespace<Remainder>> extends [
              infer Children,
              `${infer Remainder}`,
            ]
            ? Children extends Ast.Node[]
              ? // `field!left(nodes)`
                // !left is a noise word - treat it the same way as a non-`!inner`.
                [{ type: 'field'; name: Name; children: Children }, EatWhitespace<Remainder>]
              : ParserError<'Invalid children array in left join'>
            : CreateParserErrorIfRequired<
                ParseEmbeddedResource<EatWhitespace<Remainder>>,
                `Expected embedded resource after "!left" at \`${EatWhitespace<Remainder>}\``
              >
          : EatWhitespace<Remainder> extends `!${infer Remainder}`
            ? ParseIdentifier<EatWhitespace<Remainder>> extends [infer Hint, `${infer Remainder}`]
              ? EatWhitespace<Remainder> extends `!inner${infer Remainder}`
                ? ParseEmbeddedResource<EatWhitespace<Remainder>> extends [
                    infer Children,
                    `${infer Remainder}`,
                  ]
                  ? Children extends Ast.Node[]
                    ? // `field!hint!inner(nodes)`
                      [
                        {
                          type: 'field'
                          name: Name
                          hint: Hint
                          innerJoin: true
                          children: Children
                        },
                        EatWhitespace<Remainder>,
                      ]
                    : ParserError<'Invalid children array in hint inner join'>
                  : ParseEmbeddedResource<EatWhitespace<Remainder>>
                : ParseEmbeddedResource<EatWhitespace<Remainder>> extends [
                      infer Children,
                      `${infer Remainder}`,
                    ]
                  ? Children extends Ast.Node[]
                    ? // `field!hint(nodes)`
                      [
                        { type: 'field'; name: Name; hint: Hint; children: Children },
                        EatWhitespace<Remainder>,
                      ]
                    : ParserError<'Invalid children array in hint'>
                  : ParseEmbeddedResource<EatWhitespace<Remainder>>
              : ParserError<`Expected identifier after "!" at \`${EatWhitespace<Remainder>}\``>
            : EatWhitespace<Remainder> extends `(${infer _}`
              ? ParseEmbeddedResource<EatWhitespace<Remainder>> extends [
                  infer Children,
                  `${infer Remainder}`,
                ]
                ? Children extends Ast.Node[]
                  ? // `field(nodes)`
                    [{ type: 'field'; name: Name; children: Children }, EatWhitespace<Remainder>]
                  : ParserError<'Invalid children array in field'>
                : // Return error if start of embedded resource was detected but not found.
                  ParseEmbeddedResource<EatWhitespace<Remainder>>
              : // Otherwise it's a non-embedded resource field.
                ParseNonEmbeddedResourceField<Input>
    : ParserError<`Expected identifier at \`${Input}\``>

type ParseCountField<Input extends string> =
  ParseIdentifier<Input> extends ['count', `${infer Remainder}`]
    ? (
        EatWhitespace<Remainder> extends `()${infer Remainder_}`
          ? EatWhitespace<Remainder_>
          : EatWhitespace<Remainder>
      ) extends `${infer Remainder}`
      ? Remainder extends `::${infer _}`
        ? ParseFieldTypeCast<Remainder> extends [infer CastType, `${infer Remainder}`]
          ? [
              { type: 'field'; name: 'count'; aggregateFunction: 'count'; castType: CastType },
              Remainder,
            ]
          : ParseFieldTypeCast<Remainder>
        : [{ type: 'field'; name: 'count'; aggregateFunction: 'count' }, Remainder]
      : never
    : ParserError<`Expected "count" at \`${Input}\``>

/**
 * Parses an embedded resource, which is an opening `(`, followed by a sequence of
 * 0 or more 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}`
  ? EatWhitespace<Remainder> extends `)${infer Remainder}`
    ? [[], EatWhitespace<Remainder>]
    : ParseNodes<EatWhitespace<Remainder>> extends [infer Nodes, `${infer Remainder}`]
      ? Nodes extends Ast.Node[]
        ? EatWhitespace<Remainder> extends `)${infer Remainder}`
          ? [Nodes, EatWhitespace<Remainder>]
          : ParserError<`Expected ")" at \`${EatWhitespace<Remainder>}\``>
        : ParserError<'Invalid nodes array in embedded resource'>
      : ParseNodes<EatWhitespace<Remainder>>
  : ParserError<`Expected "(" at \`${Input}\``>

/**
 * Parses a field excluding embedded resources, without preceding field renaming.
 * This is one of the following:
 * - `field`
 * - `field.aggregate()`
 * - `field.aggregate()::type`
 * - `field::type`
 * - `field::type.aggregate()`
 * - `field::type.aggregate()::type`
 * - `field->json...`
 * - `field->json.aggregate()`
 * - `field->json.aggregate()::type`
 * - `field->json::type`
 * - `field->json::type.aggregate()`
 * - `field->json::type.aggregate()::type`
 */
type ParseNonEmbeddedResourceField<Input extends string> =
  ParseIdentifier<Input> extends [infer Name, `${infer Remainder}`]
    ? // Parse optional JSON path.
      (
        Remainder extends `->${infer PathAndRest}`
          ? ParseJsonAccessor<Remainder> extends [
              infer PropertyName,
              infer PropertyType,
              `${infer Remainder}`,
            ]
            ? [
                {
                  type: 'field'
                  name: Name
                  alias: PropertyName
                  castType: PropertyType
                  jsonPath: JsonPathToAccessor<
                    PathAndRest extends `${infer Path},${string}` ? Path : PathAndRest
                  >
                },
                Remainder,
              ]
            : ParseJsonAccessor<Remainder>
          : [{ type: 'field'; name: Name }, Remainder]
      ) extends infer Parsed
      ? Parsed extends [infer Field, `${infer Remainder}`]
        ? // Parse optional typecast or aggregate function input typecast.
          (
            Remainder extends `::${infer _}`
              ? ParseFieldTypeCast<Remainder> extends [infer CastType, `${infer Remainder}`]
                ? [Omit<Field, 'castType'> & { castType: CastType }, Remainder]
                : ParseFieldTypeCast<Remainder>
              : [Field, Remainder]
          ) extends infer Parsed
          ? Parsed extends [infer Field, `${infer Remainder}`]
            ? // Parse optional aggregate function.
              Remainder extends `.${infer _}`
              ? ParseFieldAggregation<Remainder> extends [
                  infer AggregateFunction,
                  `${infer Remainder}`,
                ]
                ? // Parse optional aggregate function output typecast.
                  Remainder extends `::${infer _}`
                  ? ParseFieldTypeCast<Remainder> extends [infer CastType, `${infer Remainder}`]
                    ? [
                        Omit<Field, 'castType'> & {
                          aggregateFunction: AggregateFunction
                          castType: CastType
                        },
                        Remainder,
                      ]
                    : ParseFieldTypeCast<Remainder>
                  : [Field & { aggregateFunction: AggregateFunction }, Remainder]
                : ParseFieldAggregation<Remainder>
              : [Field, Remainder]
            : Parsed
          : never
        : Parsed
      : never
    : 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"]
 */
type ParseJsonAccessor<Input extends string> = Input extends `->${infer Remainder}`
  ? Remainder extends `>${infer Remainder}`
    ? ParseIdentifier<Remainder> extends [infer Name, `${infer Remainder}`]
      ? [Name, 'text', 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 `->`'>
  : ParserError<'Expected ->'>

/**
 * Parses a field typecast (`::type`), returning a tuple of ["Type", "Remainder of text"].
 */
type ParseFieldTypeCast<Input extends string> =
  EatWhitespace<Input> extends `::${infer Remainder}`
    ? ParseIdentifier<EatWhitespace<Remainder>> extends [`${infer CastType}`, `${infer Remainder}`]
      ? [CastType, EatWhitespace<Remainder>]
      : ParserError<`Invalid type for \`::\` operator at \`${Remainder}\``>
    : ParserError<'Expected ::'>

/**
 * Parses a field aggregation (`.max()`), returning a tuple of ["Aggregate function", "Remainder of text"]
 */
type ParseFieldAggregation<Input extends string> =
  EatWhitespace<Input> extends `.${infer Remainder}`
    ? ParseIdentifier<EatWhitespace<Remainder>> extends [
        `${infer FunctionName}`,
        `${infer Remainder}`,
      ]
      ? // Ensure that aggregation function is valid.
        FunctionName extends Token.AggregateFunction
        ? EatWhitespace<Remainder> extends `()${infer Remainder}`
          ? [FunctionName, EatWhitespace<Remainder>]
          : ParserError<`Expected \`()\` after \`.\` operator \`${FunctionName}\``>
        : ParserError<`Invalid type for \`.\` operator \`${FunctionName}\``>
      : ParserError<`Invalid type for \`.\` operator at \`${Remainder}\``>
    : ParserError<'Expected .'>

/**
 * Parses a (possibly double-quoted) identifier.
 * Identifiers are sequences of 1 or more letters.
 */
type ParseIdentifier<Input extends string> =
  ParseLetters<Input> extends [infer Name, `${infer Remainder}`]
    ? [Name, EatWhitespace<Remainder>]
    : ParseQuotedLetters<Input> extends [infer Name, `${infer Remainder}`]
      ? [Name, EatWhitespace<Remainder>]
      : ParserError<`No (possibly double-quoted) identifier at \`${Input}\``>

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

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

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

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

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

/**
 * Creates a new {@link ParserError} if the given input is not already a parser error.
 */
type CreateParserErrorIfRequired<Input, Message extends string> =
  Input extends ParserError<string> ? Input : ParserError<Message>

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

export namespace Ast {
  export type Node = FieldNode | StarNode | SpreadNode

  export type FieldNode = {
    type: 'field'
    name: string
    alias?: string
    hint?: string
    innerJoin?: true
    castType?: string
    jsonPath?: string
    aggregateFunction?: Token.AggregateFunction
    children?: Node[]
  }

  export type StarNode = {
    type: 'star'
  }

  export type SpreadNode = {
    type: 'spread'
    target: FieldNode & { children: Node[] }
  }
}

namespace Token {
  export 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'

  export type Letter = Alphabet | Digit | '_'

  export type AggregateFunction = 'count' | 'sum' | 'avg' | 'min' | 'max'
}
