import ts from "typescript";

import { ExpressionFactory } from "../../factories/ExpressionFactory";
import { IdentifierFactory } from "../../factories/IdentifierFactory";
import { MetadataCollection } from "../../factories/MetadataCollection";
import { MetadataFactory } from "../../factories/MetadataFactory";
import { StatementFactory } from "../../factories/StatementFactory";
import { TypeFactory } from "../../factories/TypeFactory";

import { Metadata } from "../../schemas/metadata/Metadata";
import { MetadataArrayType } from "../../schemas/metadata/MetadataArrayType";
import { MetadataObject } from "../../schemas/metadata/MetadataObject";
import { MetadataProperty } from "../../schemas/metadata/MetadataProperty";

import { IProject } from "../../transformers/IProject";
import { TransformerError } from "../../transformers/TransformerError";

import { Atomic } from "../../typings/Atomic";

import { Escaper } from "../../utils/Escaper";
import { MapUtil } from "../../utils/MapUtil";

import { FeatureProgrammer } from "../FeatureProgrammer";
import { FunctionImporter } from "../helpers/FunctionImporter";
import { HttpMetadataUtil } from "../helpers/HttpMetadataUtil";

export namespace HttpHeadersProgrammer {
  export const INPUT_TYPE = "Record<string, string | string[] | undefined>";

  export const decompose = (props: {
    project: IProject;
    importer: FunctionImporter;
    type: ts.Type;
    name: string | undefined;
  }): FeatureProgrammer.IDecomposed => {
    // ANALYZE TYPE
    const collection: MetadataCollection = new MetadataCollection();
    const result = MetadataFactory.analyze(
      props.project.checker,
      props.project.context,
    )({
      escape: false,
      constant: true,
      absorb: true,
      validate,
    })(collection)(props.type);
    if (result.success === false)
      throw TransformerError.from(`typia.http.${props.importer.method}`)(
        result.errors,
      );

    // DO TRANSFORM
    const object: MetadataObject = result.data.objects[0]!;
    const statements: ts.Statement[] = decode_object(props.importer)(object);
    return {
      functions: {},
      statements: [],
      arrow: ts.factory.createArrowFunction(
        undefined,
        undefined,
        [
          IdentifierFactory.parameter(
            "input",
            ts.factory.createTypeReferenceNode(INPUT_TYPE),
          ),
        ],
        ts.factory.createImportTypeNode(
          ts.factory.createLiteralTypeNode(
            ts.factory.createStringLiteral("typia"),
          ),
          undefined,
          ts.factory.createIdentifier("Resolved"),
          [
            ts.factory.createTypeReferenceNode(
              props.name ??
                TypeFactory.getFullName(props.project.checker)(props.type),
            ),
          ],
          false,
        ),
        undefined,
        ts.factory.createBlock(statements, true),
      ),
    };
  };

  export const write =
    (project: IProject) =>
    (modulo: ts.LeftHandSideExpression) =>
    (type: ts.Type, name?: string): ts.CallExpression => {
      const importer: FunctionImporter = new FunctionImporter(modulo.getText());
      const result: FeatureProgrammer.IDecomposed = decompose({
        project,
        importer,
        type,
        name,
      });
      return FeatureProgrammer.writeDecomposed({
        modulo,
        importer,
        result,
      });
    };

  export const validate = (
    meta: Metadata,
    explore: MetadataFactory.IExplore,
  ): string[] => {
    const errors: string[] = [];
    const insert = (msg: string) => errors.push(msg);

    if (explore.top === true) {
      // TOP MUST BE ONLY OBJECT
      if (meta.objects.length !== 1 || meta.bucket() !== 1)
        insert("only one object type is allowed.");
      if (meta.nullable === true) insert("headers cannot be null.");
      if (meta.isRequired() === false) insert("headers cannot be null.");
    } else if (
      explore.nested !== null &&
      explore.nested instanceof MetadataArrayType
    ) {
      //----
      // ARRAY
      //----
      const atomics = HttpMetadataUtil.atomics(meta);
      const expected: number =
        meta.atomics.length +
        meta.templates.length +
        meta.constants.map((c) => c.values.length).reduce((a, b) => a + b, 0);
      if (atomics.size > 1) insert("union type is not allowed in array.");
      if (meta.size() !== expected)
        insert("only atomic or constant types are allowed in array.");
      if (meta.nullable === true)
        insert("nullable type is not allowed in array.");
      if (meta.isRequired() === false)
        insert("optional type is not allowed in array.");
    } else if (explore.object && explore.property !== null) {
      //----
      // COMMON
      //----
      // PROPERTY MUST BE SOLE
      if (typeof explore.property === "object")
        insert("dynamic property is not allowed.");
      // DO NOT ALLOW TUPLE TYPE
      if (meta.tuples.length) insert("tuple type is not allowed.");
      // DO NOT ALLOW UNION TYPE
      if (HttpMetadataUtil.isUnion(meta)) insert("union type is not allowed.");
      // DO NOT ALLOW NESTED OBJECT
      if (
        meta.objects.length ||
        meta.sets.length ||
        meta.maps.length ||
        meta.natives.length
      )
        insert("nested object type is not allowed.");
      // DO NOT ALLOW NULLABLE
      if (meta.nullable === true) insert("nullable type is not allowed.");

      //----
      // SPECIAL KEY NAMES
      //----
      const isArray: boolean =
        meta.arrays.length >= 1 || meta.tuples.length >= 1;
      // SET-COOKIE MUST BE ARRAY
      if (
        typeof explore.property === "string" &&
        explore.property.toLowerCase() === "set-cookie" &&
        isArray === false
      )
        insert(`${explore.property} property must be array.`);
      // MUST BE SINGULAR CASE
      if (
        typeof explore.property === "string" &&
        SINGULAR.has(explore.property.toLowerCase()) &&
        isArray === true
      )
        insert("property cannot be array.");
    } else if (explore.object && explore.property === null) {
      const counter: Map<string, Set<string>> = new Map();
      for (const prop of explore.object.properties) {
        const key: string | null = prop.key.getSoleLiteral();
        if (key === null) continue;

        MapUtil.take(counter)(key.toLowerCase(), () => new Set()).add(key);
      }
      for (const [key, set] of counter)
        if (set.size > 1)
          insert(
            `duplicated keys when converting to lowercase letters: [${[
              ...set,
            ].join(", ")}] -> ${key}`,
          );
    }
    return errors;
  };

  const decode_object =
    (importer: FunctionImporter) =>
    (object: MetadataObject): ts.Statement[] => {
      const output: ts.Identifier = ts.factory.createIdentifier("output");
      const optionals: string[] = [];
      return [
        StatementFactory.constant(
          "output",
          ts.factory.createObjectLiteralExpression(
            object.properties.map((prop) => {
              if (
                !prop.value.isRequired() &&
                prop.value.arrays.length + prop.value.tuples.length > 0
              )
                optionals.push(
                  prop.key.constants[0]!.values[0]!.value as string,
                );
              return decode_regular_property(importer)(prop);
            }),
            true,
          ),
        ),
        ...optionals.map((key) => {
          const access = IdentifierFactory.access(output)(key);
          return ts.factory.createIfStatement(
            ts.factory.createStrictEquality(
              ExpressionFactory.number(0),
              IdentifierFactory.access(access)("length"),
            ),
            ts.factory.createExpressionStatement(
              ts.factory.createDeleteExpression(access),
            ),
          );
        }),
        ts.factory.createReturnStatement(
          ts.factory.createAsExpression(output, TypeFactory.keyword("any")),
        ),
      ];
    };

  const decode_regular_property =
    (importer: FunctionImporter) =>
    (property: MetadataProperty): ts.PropertyAssignment => {
      const key: string = property.key.constants[0]!.values[0]!.value as string;
      const value: Metadata = property.value;

      const [type, isArray]: [Atomic.Literal, boolean] = value.atomics.length
        ? [value.atomics[0]!.type, false]
        : value.constants.length
          ? [value.constants[0]!.type, false]
          : value.templates.length
            ? ["string", false]
            : (() => {
                const meta: Metadata =
                  value.arrays[0]?.type.value ??
                  value.tuples[0]!.type.elements[0]!;
                return meta.atomics.length
                  ? [meta.atomics[0]!.type, true]
                  : meta.templates.length
                    ? ["string", true]
                    : [meta.constants[0]!.type, true];
              })();
      const accessor = IdentifierFactory.access(
        ts.factory.createIdentifier("input"),
      )(key.toLowerCase());

      return ts.factory.createPropertyAssignment(
        Escaper.variable(key) ? key : ts.factory.createStringLiteral(key),
        isArray
          ? key === "set-cookie"
            ? accessor
            : decode_array(importer)(type)(key)(value)(accessor)
          : decode_value(importer)(type)(accessor),
      );
    };

  const decode_value =
    (importer: FunctionImporter) =>
    (type: Atomic.Literal) =>
    (value: ts.Expression) =>
      type === "string"
        ? value
        : ts.factory.createCallExpression(importer.use(type), undefined, [
            value,
          ]);

  const decode_array =
    (importer: FunctionImporter) =>
    (type: Atomic.Literal) =>
    (key: string) =>
    (value: Metadata) =>
    (accessor: ts.Expression) => {
      const split: ts.CallChain = ts.factory.createCallChain(
        ts.factory.createPropertyAccessChain(
          ts.factory.createCallChain(
            ts.factory.createPropertyAccessChain(
              accessor,
              ts.factory.createToken(ts.SyntaxKind.QuestionDotToken),
              ts.factory.createIdentifier("split"),
            ),
            undefined,
            undefined,
            [ts.factory.createStringLiteral(key === "cookie" ? "; " : ", ")],
          ),
          ts.factory.createToken(ts.SyntaxKind.QuestionDotToken),
          ts.factory.createIdentifier("map"),
        ),
        undefined,
        undefined,
        [importer.use(type)],
      );
      return ts.factory.createConditionalExpression(
        ExpressionFactory.isArray(accessor),
        undefined,
        ts.factory.createCallExpression(
          IdentifierFactory.access(accessor)("map"),
          undefined,
          [importer.use(type)],
        ),
        undefined,
        value.isRequired() === false
          ? split
          : ts.factory.createBinaryExpression(
              split,
              ts.factory.createToken(ts.SyntaxKind.QuestionQuestionToken),
              ts.factory.createArrayLiteralExpression([], false),
            ),
      );
    };
}

const SINGULAR: Set<string> = new Set([
  "age",
  "authorization",
  "content-length",
  "content-type",
  "etag",
  "expires",
  "from",
  "host",
  "if-modified-since",
  "if-unmodified-since",
  "last-modified",
  "location",
  "max-forwards",
  "proxy-authorization",
  "referer",
  "retry-after",
  "server",
  "user-agent",
]);
