/*!
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

const ts = require("typescript");
const fs = require("fs");

/* This library does three things to make openapi-typescript generation easier to use.
 * 1. Creates capitalized exports for Paths and Operations
 * 2. Alias Variables based either on the Path name or the Operation ID, such as:
 *      export type ListProjectsVariables = operations['listProjects']['parameters']['query'];
 * 3. Aliases the returned data types, such as:
 *      export type ConnectionArgument = components['schemas']['ConnectionArgument'];
 */

/* Finds all words, capitalizes them, and removes all other characters. */
const toPascalCase = (str) =>
  (str.match(/[a-zA-Z0-9]+/g) || [])
    .map((w) => `${w.charAt(0).toUpperCase()}${w.slice(1)}`)
    .join("");

/* Adds a prefix to a type prop as necessary.
 * ('', 'components') => 'components'
 */
const prefixPath = (rootPrefix, prop) =>
  rootPrefix ? `${rootPrefix}['${prop}']` : prop;

// Recursively find child nodes by name.
const findNode = (node, ...names) => {
  if (!node || names.length === 0) return node;

  const children = node.members || node.type.members;

  if (!children) {
    return undefined;
  }

  const child = children.find((c) => c.name?.text === names[0]);

  return findNode(child, ...names.slice(1));
};

// Generate Variable Type Aliases for a given path or operation
const generateVariableAliases = (node, operationPath, operationName) => {
  const variableTypes = [];
  const hasPath = !!findNode(node, "parameters", "path");
  const hasQuery = !!findNode(node, "parameters", "query");
  const hasBody = !!findNode(
    node,
    "requestBody",
    "content",
    "application/json"
  );

  if (hasPath) variableTypes.push(`${operationPath}['parameters']['path']`);
  if (hasQuery) variableTypes.push(`${operationPath}['parameters']['query']`);
  if (hasBody)
    variableTypes.push(
      `${operationPath}['requestBody']['content']['application/json']`
    );

  if (variableTypes.length === 0) return "";
  const typeName = `${toPascalCase(operationName)}Variables`;
  return [
    typeName,
    `export type ${typeName} = CamelCasedPropertiesDeep<${variableTypes.join(
      " & "
    )}>;`,
  ];
};

// Generate Type Aliases
const generateAliases = (rootNode, writeText, prefix = "") => {
  // Loop through the root AST nodes of the file
  ts.forEachChild(rootNode, (node) => {
    // Response Data Types
    if (ts.isInterfaceDeclaration(node) && node.name?.text === "components") {
      const schemaMemberNames = findNode(node, "schemas").type.members.map(
        (n) => n.name?.text
      );

      const types = schemaMemberNames.map((n) => [
        `${n}`,
        `export type ${n} = CamelCasedPropertiesDeep<${prefixPath(
          prefix,
          "components"
        )}['schemas']['${n}']>;`,
      ]);
      if (types.length) {
        writeText.push(["comment", `Types for returned data ${prefix}`]);
        writeText.push(...types);
      }
    }

    // Paths referencing an operation are skipped
    if (node.name?.text === "paths") {
      if (!prefix) {
        writeText.push(["comment", "Alias paths to PascalCase."]);
        writeText.push(["Paths", "export type Paths = paths;"]);
      }

      const types = [];

      (node.members || node.type.members).forEach((path) => {
        const methodNames = path.type.members.map((m) => m.name.text);
        const methodTypes = methodNames.map((m) =>
          generateVariableAliases(
            findNode(path, m),
            `${prefixPath(prefix, "paths")}['${path.name?.text}']['${m}']`,
            `${path.name.text}${toPascalCase(m)}`
          )
        );
        types.push(...methodTypes.filter((m) => !!m));
      });

      if (types.length) {
        writeText.push([
          "comment",
          `Types for path operation variables ${prefix}`,
        ]);
        writeText.push(...types);
      }
    }

    // operationIds are defined
    if (node.name?.text === "operations") {
      if (!prefix) {
        writeText.push(["comment", "Alias operations to PascalCase."]);
        writeText.push(["Operations", "export type Operations = operations;"]);
      }

      const types = (node.members || node.type.members).map((operation) =>
        generateVariableAliases(
          operation,
          `${prefixPath(prefix, "operations")}['${operation.name.text}']`,
          operation.name.text
        )
      );
      if (types.length) {
        writeText.push(["comment", `Types for operation variables ${prefix}`]);
        writeText.push(...types);
        writeText.push("\n");
      }
    }

    // recursively call this for any externals
    if (ts.isInterfaceDeclaration(node) && node.name?.text === "external") {
      node.members.forEach((external) => {
        generateAliases(
          external.type,
          writeText,
          `external['${external.name.text}']`
        );
      });
    }
  });
};

const license = `/*!
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements.  See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership.  The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License.  You may obtain a copy of the License at
*
*   http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied.  See the License for the
* specific language governing permissions and limitations
* under the License.
*/`;

function generate(file) {
  // Create a Program to represent the project, then pull out the
  // source file to parse its AST.
  const program = ts.createProgram([file], { allowJs: true });
  const sourceFile = program.getSourceFile(file);
  const writeText = [];
  writeText.push(["block", license]);
  writeText.push(["comment", "eslint-disable"]);
  // eslint-disable-next-line quotes
  writeText.push([
    "block",
    `import type { CamelCasedPropertiesDeep } from 'type-fest';`,
  ]);
  writeText.push(["block", sourceFile.text]);
  generateAliases(sourceFile, writeText);

  const finalText = writeText
    // Deduplicate types
    .map((pair) => {
      // keep all comments and code blocks
      if (pair[0] === "comment" || pair[0] === "block") return pair;
      // return the first instance of this key only
      const firstInstance = writeText.find((p) => p[0] === pair[0]);
      return firstInstance === pair
        ? pair
        : ["comment", `Duplicate removed: ${pair[1]}`];
    })
    // Remove undefined created above
    .filter((p) => !!p)
    // Escape comments and flatten.
    .map((pair) => (pair[0] === "comment" ? `\n/* ${pair[1]} */` : pair[1]))
    .join("\n");

  fs.writeFileSync(file, finalText, (err) => {
    if (err) {
      // eslint-disable-next-line no-console
      console.error(err);
    }
  });
}

generate(process.argv[2]);
