import assert from "assert";
import inflection from "inflection";
import { diff, group, sort, unique } from "radashi";
import {
  apiParamToTsCode,
  apiParamToTsCodeAsObject,
  apiParamTypeToTsType,
  unwrapPromiseOnce,
} from "../../api/code-converters";
import type { ExtendedApi } from "../../api/decorators";
import { Sonamu } from "../../api/sonamu";
import type { EntityNamesRecord } from "../../entity/entity-manager";
import { Naite } from "../../naite/naite";
import type { TemplateOptions } from "../../types/types";
import { type ApiParam, ApiParamType } from "../../types/types";
import { assertDefined } from "../../utils/utils";
import { Template } from "../template";
import { zodTypeToTsTypeDef } from "../zod-converter";

export class Template__service extends Template {
  constructor() {
    super("service");
  }

  getTargetAndPath(names: EntityNamesRecord) {
    return {
      target: ":target/src/services",
      path: `${names.fs}/${names.fs}.service.ts`,
    };
  }

  render({ namesRecord }: TemplateOptions["service"]) {
    Naite.t("render", { namesRecord });

    const {
      syncer: { apis },
    } = Sonamu;

    const apisForThisModel = apis.filter(
      (api) =>
        api.modelName === `${namesRecord.capital}Model` ||
        api.modelName === `${namesRecord.capital}Frame`,
    );

    // 서비스 TypeSource
    const { lines, importKeys } = this.getTypeSource(apisForThisModel);

    // AxiosProgressEvent 있는지 확인
    const hasAxiosProgressEvent = apis.find((api) =>
      (api.options.clients ?? []).includes("axios-multipart"),
    );

    return {
      ...this.getTargetAndPath(namesRecord),
      body: lines.join("\n"),
      importKeys: importKeys.filter((key) => ["ListResult"].includes(key) === false),
      customHeaders: [
        `import { z } from 'zod';`,
        `import qs from "qs";`,
        `import useSWR, { type SWRResponse } from "swr";`,
        `import { fetch, ListResult, SWRError, SwrOptions, handleConditional, swrPostFetcher, EventHandlers, SSEStreamOptions, useSSEStream } from '../sonamu.shared';`,
        ...(hasAxiosProgressEvent ? [`import { type AxiosProgressEvent } from 'axios';`] : []),
      ],
    };
  }

  getTypeSource(apis: ExtendedApi[]): {
    lines: string[];
    importKeys: string[];
  } {
    const importKeys: string[] = [];

    // 제네릭에서 선언한 타입, importKeys에서 제외 필요
    let typeParamNames: string[] = [];

    const groups = group(apis, (api) => api.modelName);
    const body = Object.keys(groups)
      .map((modelName) => {
        const methods = groups[modelName];
        assert(methods);
        const methodCodes = methods
          .map((api) => {
            // 컨텍스트 제외된 파라미터 리스트
            const paramsWithoutContext = api.parameters.filter(
              (param) =>
                !ApiParamType.isContext(param.type) &&
                !ApiParamType.isRefKnex(param.type) &&
                !(param.optional === true && param.name.startsWith("_")), // _로 시작하는 파라미터는 제외
            );

            // 파라미터 타입 정의
            const typeParametersAsTsType = api.typeParameters
              .map((typeParam) => {
                return apiParamTypeToTsType(typeParam, importKeys);
              })
              .join(", ");
            const typeParamsDef = typeParametersAsTsType ? `<${typeParametersAsTsType}>` : "";
            typeParamNames = typeParamNames.concat(
              api.typeParameters.map((typeParam) => typeParam.id),
            );

            // 파라미터 정의
            const paramsDef = apiParamToTsCode(paramsWithoutContext, importKeys);

            // 파라미터 정의 (객체 형태)
            const paramsDefAsObject = apiParamToTsCodeAsObject(paramsWithoutContext, importKeys);

            // 리턴 타입 정의
            const returnTypeDef = apiParamTypeToTsType(
              assertDefined(unwrapPromiseOnce(api.returnType)),
              importKeys,
            );

            // 페이로드 데이터 정의
            const payloadDef = `{ ${paramsWithoutContext.map((param) => param.name).join(", ")} }`;

            // 기본 URL
            const apiBaseUrl = `${Sonamu.config.api.route.prefix}${api.path}`;

            const clients = api.options.clients ?? [];
            return [
              // 클라이언트별로 생성
              ...sort(clients, (client) => (client === "swr" ? 0 : 1)).map((client) => {
                switch (client) {
                  case "axios":
                    return this.renderAxios(
                      api,
                      apiBaseUrl,
                      typeParamsDef,
                      paramsDef,
                      returnTypeDef,
                      payloadDef,
                    );
                  case "axios-multipart":
                    return this.renderAxiosMultipart(
                      api,
                      apiBaseUrl,
                      typeParamsDef,
                      paramsDef,
                      returnTypeDef,
                      paramsWithoutContext,
                    );
                  case "swr":
                    return this.renderSwr(
                      api,
                      apiBaseUrl,
                      typeParamsDef,
                      paramsDef,
                      returnTypeDef,
                      payloadDef,
                    );
                  case "window-fetch":
                    return this.renderWindowFetch(
                      api,
                      apiBaseUrl,
                      typeParamsDef,
                      paramsDef,
                      payloadDef,
                    );
                  default:
                    return `// Not supported ${inflection.camelize(client, true)} yet.`;
                }
              }),
              // 스트리밍인 경우
              ...(api.streamOptions ? [this.renderStream(api, apiBaseUrl, paramsDefAsObject)] : []),
            ].join("\n");
          })
          .join("\n\n");

        return `export namespace ${modelName.replace(/Model$/, "Service").replace(/Frame$/, "Service")} {
${methodCodes}
}`;
      })
      .join("\n\n");

    return {
      lines: [body],
      importKeys: diff(unique(importKeys), typeParamNames),
    };
  }

  renderAxios(
    api: ExtendedApi,
    apiBaseUrl: string,
    typeParamsDef: string,
    paramsDef: string,
    returnTypeDef: string,
    payloadDef: string,
  ) {
    const methodNameAxios = api.options.resourceName
      ? `get${inflection.camelize(api.options.resourceName)}`
      : api.methodName;

    if (api.options.httpMethod === "GET") {
      return `
export async function ${methodNameAxios}${typeParamsDef}(${paramsDef}): Promise<${returnTypeDef}> {
    return fetch({
      method: "GET",
      url: \`${apiBaseUrl}?\${qs.stringify(${payloadDef})}\`,
      ${api.options.timeout ? `signal: AbortSignal.timeout(${api.options.timeout}),` : ""}
    });
}
    `.trim();
    } else {
      return `
export async function ${methodNameAxios}${typeParamsDef}(${paramsDef}): Promise<${returnTypeDef}> {
    return fetch({
      method: '${api.options.httpMethod}',
      url: \`${apiBaseUrl}\`,
      data: ${payloadDef},
      ${api.options.timeout ? `signal: AbortSignal.timeout(${api.options.timeout}),` : ""}
    });
}
      `.trim();
    }
  }

  renderAxiosMultipart(
    api: ExtendedApi,
    apiBaseUrl: string,
    typeParamsDef: string,
    paramsDef: string,
    returnTypeDef: string,
    paramsWithoutContext: ApiParam[],
  ) {
    const isMultiple = api.uploadOptions?.mode === "multiple";
    const fileParamName = isMultiple ? "files" : "file";
    const fileParamType = isMultiple ? "File[]" : "File";

    const formDataDef = isMultiple
      ? [
          `${fileParamName}.forEach(f => { formData.append("${fileParamName}", f) } ); `,
          ...paramsWithoutContext.map(
            (param) => `formData.append('${param.name}', String(${param.name}));`,
          ),
        ].join("\n")
      : [
          `formData.append("${fileParamName}", ${fileParamName});`,
          ...paramsWithoutContext.map(
            (param) => `formData.append('${param.name}', String(${param.name}));`,
          ),
        ].join("\n");

    const paramsDefComma = paramsDef !== "" ? ", " : "";
    return `
export async function ${api.methodName}${typeParamsDef}(
  ${paramsDef}${paramsDefComma}
  ${fileParamName}: ${fileParamType},
  onUploadProgress?: (pe:AxiosProgressEvent) => void
  ): Promise<${returnTypeDef}> {
    const formData = new FormData();
    ${formDataDef}
    return fetch({
      method: 'POST',
      url: \`${apiBaseUrl}\`,
      onUploadProgress,
      data: formData,
      ${api.options.timeout ? `signal: AbortSignal.timeout(${api.options.timeout}),` : ""}
    });
  }
  `.trim();
  }

  renderSwr(
    api: ExtendedApi,
    apiBaseUrl: string,
    typeParamsDef: string,
    paramsDef: string,
    returnTypeDef: string,
    payloadDef: string,
  ) {
    const methodNameSwr = api.options.resourceName
      ? `use${inflection.camelize(api.options.resourceName)}`
      : `use${inflection.camelize(api.methodName)}`;
    return `  export function ${inflection.camelize(methodNameSwr, true)}${typeParamsDef}(${[
      paramsDef,
      "swrOptions?: SwrOptions",
    ]
      .filter((p) => p !== "")
      .join(",")}, ): SWRResponse<${returnTypeDef}, SWRError> {
    return useSWR(handleConditional([
      \`${apiBaseUrl}\`,
      ${payloadDef},
    ], swrOptions?.conditional)${api.options.httpMethod === "POST" ? ", swrPostFetcher" : ""}${
      api.options.timeout ? `, { loadingTimeout: ${api.options.timeout} }` : ""
    });
  }`;
  }

  renderWindowFetch(
    api: ExtendedApi,
    apiBaseUrl: string,
    typeParamsDef: string,
    paramsDef: string,
    payloadDef: string,
  ) {
    return `
export async function ${api.methodName}${typeParamsDef}(${paramsDef}): Promise<Response> {
    return window.fetch(\`${apiBaseUrl}?\${qs.stringify(${payloadDef})}\`${
      api.options.timeout ? `, { signal: AbortSignal.timeout(${api.options.timeout}) }` : ""
    });
}
    `.trim();
  }

  renderStream(api: ExtendedApi, apiBaseUrl: string, paramsDefAsObject: string) {
    if (!api.streamOptions) {
      return "// streamOptions not found";
    }

    const methodNameStream = api.options.resourceName
      ? `use${inflection.camelize(api.options.resourceName)}`
      : `use${inflection.camelize(api.methodName)}`;
    const methodNameStreamCamelized = inflection.camelize(methodNameStream, true);

    const eventsTypeDef = zodTypeToTsTypeDef(api.streamOptions.events);

    return `  export function ${methodNameStreamCamelized}(
  params: ${paramsDefAsObject},
  handlers: EventHandlers<${eventsTypeDef} & { end?: () => void }>,
  options: SSEStreamOptions) {
    return useSSEStream<${eventsTypeDef}>(\`${apiBaseUrl}\`, params, handlers, options);
  }`;
  }
}
