import { WSDL, Client } from 'soap';
import formatXml from 'xml-formatter';
import { fileURLToPath } from 'url';
import path, { resolve, join } from 'node:path';

import type { ExternalPlugin } from '@redocly/realm/dist/server/types';
import type { Logger } from '@redocly/realm/dist/server/tools/notifiers/logger';
import type { Feature } from '@redocly/realm/dist/types';
import type { RedoclyConfig } from '@redocly/config';

import { searchResolver } from './search-resolver.js';

const SOAP_TEMPLATE_ID = 'soap-docs';

const STUB_SOAP_RESPONSE = `HTTP/1.0 200 OK
Content-Type: text/xml; charset = utf-8
Content-Length: 42

<?xml version = "1.0"?>
<SOAP-ENV:Envelope
   xmlns:SOAP-ENV = "http://www.w3.org/2001/12/soap-envelope"
   SOAP-ENV:encodingStyle = "http://www.w3.org/2001/12/soap-encoding">

   <SOAP-ENV:Body xmlns:m = "http://www.xyz.org/quotation">
   </SOAP-ENV:Body>
</SOAP-ENV:Envelope>`;

export default function soapPlugin(): ExternalPlugin {
  return {
    id: 'soap',
    requiredEntitlements: ['soap' as Feature],
    loaders: {
      wsdl: async (relativePath, context) => {
        const config = await context.getConfig(relativePath);
        const { metadata } = getApiConfigByPath(config.apis, relativePath);
        const content = await context.fs.read(relativePath);

        const wsdl = (await new Promise((resolve) => {
          const wsdl = new WSDL(content, relativePath, {});
          wsdl.onReady(() => resolve(wsdl));
        })) as WSDL;

        const info = new Client(wsdl, undefined, {
          httpClient: {
            request: (
              _location: any,
              _data: any,
              cb: (err: any, res: any, body: string) => void,
            ): any => {
              cb(undefined, { status: 200 }, STUB_SOAP_RESPONSE);
            },
          },
        });
        const meta = info.describe();
        const serviceName = Object.keys(meta)[0];
        const bindingName = Object.keys(meta[serviceName])[0];
        const endpoints = meta[serviceName][bindingName];

        return {
          endpoints,
          metadata,
          serviceName,
          info,
          content,
        };
      },
    },

    processContent: async (actions: any, context: any) => {
      const soapTemplateId = actions.createTemplate(
        SOAP_TEMPLATE_ID,
        fromCurrentDir(import.meta.url, './template.js'),
      );

      for (const fileInfo of await context.fs.scan(/(\.wsdl)$/)) {
        if (await context.isPathIgnored(fileInfo.relativePath)) {
          continue;
        }

        const wsdlRecord = await context.cache.load(fileInfo.relativePath, 'wsdl');
        const { endpoints, metadata, serviceName, info, content } = wsdlRecord.data;

        for (const endpointName of Object.keys(endpoints ?? {})) {
          /**
           * Add a page for operation
           */
          actions.addRoute({
            fsPath: fileInfo.relativePath,
            slugSuffix: slugFromEndpoint(endpointName),
            templateId: soapTemplateId,
            getNavText: async () => endpointName,
            getSearchDocuments: searchResolver(actions),
            getStaticData: async () => {
              return {
                props: {
                  title: endpointName,
                  meta: {},
                  endpointName,
                  request: await generateRequestExample(
                    endpointName,
                    info,
                    endpoints[endpointName].input,
                    context.logger,
                  ),
                },
              };
            },
          });
        }

        /**
         * Add a route with general information
         */
        actions.addRoute({
          fsPath: fileInfo.relativePath,
          templateId: soapTemplateId,
          metadata: {
            type: 'soap',
            title: serviceName,
            description: '',
            tags: ['SOAP'],
            ...metadata,
          },
          getSidebar: (route: any) => {
            const operationSidebarItems = [];

            for (const endpointName of Object.keys(endpoints)) {
              const routeSlug = join(route.slug, slugFromEndpoint(endpointName));
              operationSidebarItems.push({
                type: 'link',
                link: routeSlug,
                label: endpointName,
                routeSlug,
              });
            }

            return [
              {
                type: 'link',
                link: route.slug,
                routeSlug: route.slug,
                label: serviceName,
              },
              {
                type: 'group',
                label: 'Operations',
                items: operationSidebarItems,
              },
            ];
          },
          getNavText: async () => serviceName,
          getSearchDocuments: searchResolver(actions),
          getStaticData: async () => {
            return {
              props: {
                title: serviceName,
                wsdl: formatXml(content, { indentation: '  ' }),
              },
            };
          },
        });
      }
    },
  };
}

/**
 * function which generates an example request XML
 */
const generateRequestExample = async (
  endpointName: string,
  info: any,
  inputParams: any,
  logger: Logger,
) => {
  const mockInput = mockProperties(endpointName, inputParams, 1, logger);

  const requestXml = (await new Promise((resolve) => {
    info[endpointName](mockInput, (...params: any[]) => {
      resolve(params[4]);
    });
  })) as string;

  return formatXml(requestXml, { indentation: '  ', throwOnFailure: false });
};

const mockProperties = (
  endpointName: string,
  inputParams: any,
  recursiveLevel: number,
  logger: Logger,
) => {
  const mockInput: Record<string, any> = {};

  if (recursiveLevel > 10) {
    return mockInput;
  }

  const paramNames = Object.keys(inputParams || {});

  for (let paramName of paramNames) {
    if (['targetNamespace', 'targetNSAlias'].includes(paramName)) {
      continue;
    }

    const paramValue = inputParams[paramName];

    if (paramName.endsWith('[]')) {
      paramName = paramName.slice(0, -2);
    }

    if (paramValue && typeof paramValue === 'object') {
      mockInput[paramName] = mockProperties(endpointName, paramValue, recursiveLevel + 1, logger);
    } else if (typeof paramValue === 'string') {
      const paramType = paramValue.split(':')[1];
      switch (paramType) {
        case 'string':
          mockInput[paramName] = 'Lorem ipsum';
          break;

        case 'date':
          mockInput[paramName] = '2023-07-06';
          break;

        case 'decimal':
          mockInput[paramName] = 42;
          break;

        default:
          mockInput[paramName] = paramName;
      }
    } else {
      const error = `Can't generate request example. Request param ${paramName} not supported in endpoint ${endpointName}`;
      mockInput[paramName] = { error };
      logger.warn(error);
      continue;
    }
  }
  return mockInput;
};

const slugFromEndpoint = (name: string): string => {
  return name
    .trim()
    .replace(/[^\w\-]/g, '-')
    .replaceAll(/-+/g, '-')
    .toLowerCase()
    .replace(/^-|-$/g, '');
};

function __dirname(url: string): string {
  const __filename = fileURLToPath(url);
  return path.dirname(__filename);
}

function fromCurrentDir(moduleUrl: string, path: string): string {
  return resolve(__dirname(moduleUrl), path);
}

function getApiConfigByPath(
  apis: RedoclyConfig['apis'],
  relativePath: string,
): Record<string, any> {
  for (const versionValue of Object.values(apis || {})) {
    if (path.posix.normalize(versionValue.root) === relativePath) {
      return {
        ...versionValue.theme,
        ...versionValue,
        theme: undefined,
      };
    }
  }
  return {};
}
