import {
  type Expression,
  NodeFlags,
  type Statement,
  SyntaxKind,
  type TypeNode,
  factory,
} from "@ttsc/factory";
import { IJsDocTagInfo } from "typia";

import { IdentifierFactory } from "../../factories/IdentifierFactory";
import { INestiaProject } from "../../structures/INestiaProject";
import { ITypedWebSocketRoute } from "../../structures/ITypedWebSocketRoute";
import { StringUtil } from "../../utils/StringUtil";
import { FilePrinter } from "./FilePrinter";
import { ImportDictionary } from "./ImportDictionary";
import { SdkImportWizard } from "./SdkImportWizard";
import { SdkWebSocketNamespaceProgrammer } from "./SdkWebSocketNamespaceProgrammer";
import { SdkWebSocketParameterProgrammer } from "./SdkWebSocketParameterProgrammer";

export namespace SdkWebSocketRouteProgrammer {
  export const write =
    (project: INestiaProject) =>
    (importer: ImportDictionary) =>
    (route: ITypedWebSocketRoute): Statement[] => [
      FilePrinter.description(
        writeFunction(project)(importer)(route),
        writeDescription(route),
      ),
      SdkWebSocketNamespaceProgrammer.write(project)(importer)(route),
    ];

  const writeDescription = (route: ITypedWebSocketRoute): string => {
    // MAIN DESCRIPTION
    const comments: string[] = route.description
      ? route.description.split("\n")
      : [];

    // COMMENT TAGS
    const tags: IJsDocTagInfo[] = route.jsDocTags.filter(
      (tag) =>
        tag.name !== "param" ||
        [...route.pathParameters, ...(route.query ? [route.query] : [])]
          .filter((p) => p.category === "param" || p.category === "query")
          .some((p) => p.name === tag.text?.[0]?.text),
    );
    if (tags.length !== 0) {
      const content: string[] = tags.map((t) =>
        t.text?.length
          ? `@${t.name} ${t.text.map((e) => e.text).join("")}`
          : `@${t.name}`,
      );
      comments.push("", ...new Set(content));
    }

    // POSTFIX
    if (!!comments.length) comments.push("");
    comments.push(
      `@controller ${route.controller.class.name}.${route.name}`,
      `@path ${route.path}`,
      `@nestia Generated by Nestia - https://github.com/samchon/nestia`,
    );
    return comments.join("\n");
  };

  const writeFunction =
    (project: INestiaProject) =>
    (importer: ImportDictionary) =>
    (route: ITypedWebSocketRoute): Statement => {
      const connection: string = StringUtil.escapeDuplicate([route.name])(
        "connection",
      );
      return factory.createFunctionDeclaration(
        [
          factory.createModifier(SyntaxKind.ExportKeyword),
          factory.createModifier(SyntaxKind.AsyncKeyword),
        ],
        undefined,
        route.name,
        undefined,
        [
          IdentifierFactory.parameter(
            connection,
            factory.createTypeReferenceNode(
              SdkImportWizard.IConnection(importer),
              [factory.createTypeReferenceNode(`${route.name}.Header`)],
            ),
          ),
          ...SdkWebSocketParameterProgrammer.getParameterDeclarations({
            project,
            route,
            provider: true,
            prefix: true,
          }),
        ],
        factory.createTypeReferenceNode("Promise", [
          factory.createTypeReferenceNode(`${route.name}.Output`),
        ]),
        factory.createBlock(
          writeFunctionBody(project)(importer)(route)(connection),
          true,
        ),
      );
    };

  const writeFunctionBody =
    (project: INestiaProject) =>
    (importer: ImportDictionary) =>
    (route: ITypedWebSocketRoute) =>
    (connection: string): Statement[] => {
      const access = (key: string) =>
        project.config.keyword === true
          ? factory.createPropertyAccessExpression(
              factory.createIdentifier("props"),
              key,
            )
          : factory.createIdentifier(key);
      return [
        local("url")(factory.createTypeReferenceNode("string"))(
          joinPath(
            connection,
            factory.createCallExpression(
              factory.createPropertyAccessExpression(
                factory.createIdentifier(route.name),
                "path",
              ),
              [],
              project.config.keyword === true &&
                SdkWebSocketParameterProgrammer.isPathEmpty(route) === false
                ? [factory.createIdentifier("props")]
                : SdkWebSocketParameterProgrammer.getEntries({
                    project,
                    route,
                    provider: false,
                    prefix: true,
                  }).map((p) => factory.createIdentifier(p.key)),
            ),
          ),
        ),
        local("connector")(
          factory.createTypeReferenceNode(
            importer.external({
              declaration: false,
              file: "tgrid",
              type: "element",
              name: "WebSocketConnector",
            }),
            [
              factory.createTypeReferenceNode(`${route.name}.Header`),
              factory.createTypeReferenceNode(`${route.name}.Provider`),
              factory.createTypeReferenceNode(`${route.name}.Listener`),
            ],
          ),
        )(
          factory.createNewExpression(
            factory.createIdentifier(
              importer.external({
                declaration: false,
                file: "tgrid",
                type: "element",
                name: "WebSocketConnector",
              }),
            ),
            undefined,
            [
              factory.createAsExpression(
                factory.createBinaryExpression(
                  factory.createPropertyAccessExpression(
                    factory.createIdentifier(connection),
                    "headers",
                  ),
                  factory.createToken(SyntaxKind.QuestionQuestionToken),
                  factory.createObjectLiteralExpression([], false),
                ),
                factory.createKeywordTypeNode(SyntaxKind.AnyKeyword),
              ),
              access("provider"),
            ],
          ),
        ),
        factory.createExpressionStatement(
          factory.createAwaitExpression(
            factory.createCallExpression(
              factory.createPropertyAccessExpression(
                factory.createIdentifier("connector"),
                "connect",
              ),
              undefined,
              [factory.createIdentifier("url")],
            ),
          ),
        ),
        local("driver")(
          factory.createTypeReferenceNode(
            importer.external({
              declaration: true,
              file: "tgrid",
              type: "element",
              name: "Driver",
            }),
            [factory.createTypeReferenceNode(`${route.name}.Listener`)],
          ),
        )(
          factory.createCallExpression(
            factory.createPropertyAccessExpression(
              factory.createIdentifier("connector"),
              "getDriver",
            ),
            undefined,
            undefined,
          ),
        ),
        factory.createReturnStatement(
          factory.createObjectLiteralExpression(
            [
              factory.createShorthandPropertyAssignment("connector"),
              factory.createShorthandPropertyAssignment("driver"),
              factory.createPropertyAssignment(
                factory.createIdentifier("reconnect"),
                factory.createArrowFunction(
                  [factory.createToken(SyntaxKind.AsyncKeyword)],
                  undefined,
                  [],
                  undefined,
                  factory.createToken(SyntaxKind.EqualsGreaterThanToken),
                  factory.createAwaitExpression(
                    factory.createCallExpression(
                      factory.createPropertyAccessExpression(
                        factory.createIdentifier("connector"),
                        factory.createIdentifier("connect"),
                      ),
                      undefined,
                      [factory.createIdentifier("url")],
                    ),
                  ),
                ),
              ),
            ],
            true,
          ),
        ),
      ];
    };
}

const local = (name: string) => (type: TypeNode) => (expression: Expression) =>
  factory.createVariableStatement(
    [],
    factory.createVariableDeclarationList(
      [factory.createVariableDeclaration(name, undefined, type, expression)],
      NodeFlags.Const,
    ),
  );

const joinPath = (connection: string, caller: Expression) =>
  factory.createTemplateExpression(factory.createTemplateHead("", ""), [
    factory.createTemplateSpan(
      factory.createConditionalExpression(
        factory.createCallExpression(
          factory.createPropertyAccessExpression(
            factory.createPropertyAccessExpression(
              factory.createIdentifier(connection),
              factory.createIdentifier("host"),
            ),
            factory.createIdentifier("endsWith"),
          ),
          undefined,
          [factory.createStringLiteral("/")],
        ),
        factory.createToken(SyntaxKind.QuestionToken),
        factory.createCallExpression(
          factory.createPropertyAccessExpression(
            factory.createPropertyAccessExpression(
              factory.createIdentifier(connection),
              factory.createIdentifier("host"),
            ),
            factory.createIdentifier("substring"),
          ),
          undefined,
          [
            factory.createNumericLiteral("0"),
            factory.createBinaryExpression(
              factory.createPropertyAccessExpression(
                factory.createPropertyAccessExpression(
                  factory.createIdentifier(connection),
                  factory.createIdentifier("host"),
                ),
                factory.createIdentifier("length"),
              ),
              factory.createToken(SyntaxKind.MinusToken),
              factory.createNumericLiteral("1"),
            ),
          ],
        ),
        factory.createToken(SyntaxKind.ColonToken),
        factory.createPropertyAccessExpression(
          factory.createIdentifier(connection),
          factory.createIdentifier("host"),
        ),
      ),
      factory.createTemplateMiddle(""),
    ),
    factory.createTemplateSpan(caller, factory.createTemplateTail("")),
  ]);
