import * as fs from 'node:fs';
import * as path from 'node:path';
import parseArguments from './node_modules/.pnpm/arg@5.0.2/node_modules/arg/index.mjs';
import { cyan, bold, dim, underline, magenta } from './node_modules/.pnpm/colorette@2.0.19/node_modules/colorette/index.mjs';
import './node_modules/.pnpm/common-tags@1.8.2/node_modules/common-tags/es/TemplateTag/TemplateTag.mjs';
import './node_modules/.pnpm/common-tags@1.8.2/node_modules/common-tags/es/commaLists/commaLists.mjs';
import './node_modules/.pnpm/common-tags@1.8.2/node_modules/common-tags/es/commaListsAnd/commaListsAnd.mjs';
import './node_modules/.pnpm/common-tags@1.8.2/node_modules/common-tags/es/commaListsOr/commaListsOr.mjs';
import './node_modules/.pnpm/common-tags@1.8.2/node_modules/common-tags/es/html/html.mjs';
import './node_modules/.pnpm/common-tags@1.8.2/node_modules/common-tags/es/safeHtml/safeHtml.mjs';
import './node_modules/.pnpm/common-tags@1.8.2/node_modules/common-tags/es/oneLine/oneLine.mjs';
import './node_modules/.pnpm/common-tags@1.8.2/node_modules/common-tags/es/oneLineTrim/oneLineTrim.mjs';
import './node_modules/.pnpm/common-tags@1.8.2/node_modules/common-tags/es/oneLineCommaLists/oneLineCommaLists.mjs';
import './node_modules/.pnpm/common-tags@1.8.2/node_modules/common-tags/es/oneLineCommaListsOr/oneLineCommaListsOr.mjs';
import './node_modules/.pnpm/common-tags@1.8.2/node_modules/common-tags/es/oneLineCommaListsAnd/oneLineCommaListsAnd.mjs';
import './node_modules/.pnpm/common-tags@1.8.2/node_modules/common-tags/es/inlineLists/inlineLists.mjs';
import './node_modules/.pnpm/common-tags@1.8.2/node_modules/common-tags/es/oneLineInlineLists/oneLineInlineLists.mjs';
import stripIndent from './node_modules/.pnpm/common-tags@1.8.2/node_modules/common-tags/es/stripIndent/stripIndent.mjs';
import './node_modules/.pnpm/common-tags@1.8.2/node_modules/common-tags/es/stripIndents/stripIndents.mjs';
import { printHelp } from './help.mjs';
import { emptyDirectory, loadTemplate, toValidPackageName, relativeDirectoryForDisplay, format, mergeWorkspaceAndProjectPackageJsons, isEmpty, createOutputTarget } from './shared.mjs';
import { getInWorkspace, getCreateAsMonorepo, getExtrasToSetup, getShouldInstall, getPackageManager } from './shared/prompts.mjs';
import { addToTsConfig } from './shared/tsconfig.mjs';
import { addToPackageManagerWorkspaces } from './shared/package-manager.mjs';
import { prompt } from './packages/cli-kit/build/esnext/prompt.esnext.mjs';

async function createApp() {
  const argv = getArgv();
  if (argv['--help']) {
    const additionalOptions = stripIndent`
      ${cyan(`--template`)}
      The template to use for your new application. If you don’t specify a template,
      this command will ask you for one instead. Must be one of the following:

      - ${bold('empty')}, a basic React app without any extra runtime dependencies
       - ${bold('basic')}, a web app with a minimal file structure
       - ${bold('graphql')}, a web app with a GraphQL API, fetched using @tanstack/react-query
       - ${bold('trpc')}, a web app with a tRPC API, fetched using @tanstack/react-query
    `;
    printHelp({
      kind: 'app',
      options: additionalOptions,
      packageManager: argv['--package-manager']?.toLowerCase()
    });
    return;
  }
  const inWorkspace = await getInWorkspace(argv);
  const name = await getName(argv, {
    inWorkspace
  });
  const directory = await getDirectory(argv, {
    name
  });
  const template = await getTemplate(argv);
  const createAsMonorepo = !inWorkspace && (await getCreateAsMonorepo(argv, {
    type: 'app',
    default: true
  }));
  const setupExtras = await getExtrasToSetup(argv, {
    inWorkspace
  });
  const shouldInstall = await getShouldInstall(argv);
  const packageManager = await getPackageManager(argv, {
    root: directory
  });
  const partOfMonorepo = inWorkspace || createAsMonorepo;
  const appDirectory = createAsMonorepo ? path.join(directory, 'app') : directory;
  if (fs.existsSync(directory)) {
    await emptyDirectory(directory);
    if (appDirectory !== directory) {
      fs.mkdirSync(appDirectory, {
        recursive: true
      });
    }
  } else {
    fs.mkdirSync(appDirectory, {
      recursive: true
    });
  }
  const rootDirectory = inWorkspace ? process.cwd() : directory;
  const outputRoot = createOutputTarget(rootDirectory);
  const appTemplate = loadTemplate(`app-${template}`);
  const workspaceTemplate = loadTemplate('workspace');
  const projectPackageJson = JSON.parse(await appTemplate.read('package.json'));
  if (!inWorkspace) {
    await workspaceTemplate.copy(directory, file => {
      // We will adjust the package.json before writing it
      return file !== 'package.json';
    });
    if (setupExtras.has('github')) {
      await loadTemplate('github').copy(directory);
    }
    if (setupExtras.has('vscode')) {
      await loadTemplate('vscode').copy(directory);
    }
  }
  if (createAsMonorepo) {
    const workspacePackageJson = JSON.parse(await workspaceTemplate.read('package.json'));
    workspacePackageJson.name = `${toValidPackageName(name)}-workspace`;
    if (template === 'graphql') {
      workspacePackageJson.devDependencies['@quilted/graphql-tools'] = projectPackageJson.devDependencies['@quilted/graphql-tools'];
    }
    const moduleRelativeToRoot = relativeDirectoryForDisplay(path.relative(directory, appDirectory));
    await outputRoot.write('pnpm-workspace.yaml', await format(`
            packages:
            - './packages/*'
            - '${moduleRelativeToRoot}'
          `, {
      as: 'yaml'
    }));
    await outputRoot.write('package.json', await format(JSON.stringify(workspacePackageJson), {
      as: 'json-stringify'
    }));
  }
  await appTemplate.copy(appDirectory, file => {
    // We need to merge the project gitignore with the workspace one
    if (file === '_gitignore') return partOfMonorepo;

    // We will adjust the package.json before writing it
    return file !== 'package.json';
  });
  if (template === 'graphql' && !inWorkspace) {
    const relativeFromRootToAppPath = filePath => path.relative(outputRoot.root, path.join(appDirectory, filePath));
    await outputRoot.write('graphql.config.ts', stripIndent`
        import {type Configuration} from '@quilted/graphql-tools/configuration';

        const configuration: Configuration = {
          schema: '${relativeFromRootToAppPath('graphql/schema.graphql')}',
          documents: ['${relativeFromRootToAppPath('**/*.graphql')}'],
          extensions: {
            quilt: {
              schema: [{kind: 'definitions', outputPath: '${relativeFromRootToAppPath('graphql/schema.ts')}'}],
            },
          },
        };
        
        export default configuration;      
      ` + '\n');
  }
  if (partOfMonorepo) {
    // Write the app’s package.json (the root one was already created)
    projectPackageJson.name = toValidPackageName(name);
    if (template === 'graphql') {
      delete projectPackageJson.devDependencies['@quilted/graphql-tools'];
    }
    await outputRoot.write(path.join(appDirectory, 'package.json'), await format(JSON.stringify(projectPackageJson), {
      as: 'json-stringify'
    }));
    await Promise.all([addToTsConfig(appDirectory, outputRoot), addToPackageManagerWorkspaces(appDirectory, outputRoot, packageManager.type)]);
  } else {
    // Write the package’s package.json by combining elements of the root and
    // package templates
    const [projectPackageJson, workspacePackageJson] = await Promise.all([appTemplate.read('package.json').then(content => JSON.parse(content)), workspaceTemplate.read('package.json').then(content => JSON.parse(content))]);
    const mergedPackageJson = mergeWorkspaceAndProjectPackageJsons(projectPackageJson, workspacePackageJson);
    mergedPackageJson.name = toValidPackageName(name);
    await outputRoot.write('package.json', await format(JSON.stringify(mergedPackageJson), {
      as: 'json-stringify'
    }));
    if (await appTemplate.has('_gitignore')) {
      await outputRoot.write(path.join(appDirectory, '.gitignore'), `${await outputRoot.read('.gitignore')}\n${await appTemplate.read('_gitignore')}`);
    }
  }
  if (shouldInstall) {
    console.log();
    // TODO: better loading, handle errors
    await packageManager.install();
  }
  const commands = [];
  if (!inWorkspace && directory !== process.cwd()) {
    commands.push(`cd ${cyan(relativeDirectoryForDisplay(path.relative(process.cwd(), directory)))} ${dim('# Move into your new app’s directory')}`);
  }
  if (!shouldInstall) {
    commands.push(`${packageManager.commands.install()} ${dim('# Install all your dependencies')}`);
  }
  commands.push(`${packageManager.commands.run('develop')} ${dim('# Start the development server')}`);
  const whatsNext = stripIndent`
    Your new app is ready to go! There’s just ${commands.length > 1 ? 'a few more steps' : 'one more step'} you’ll need to take
    in order to start developing:
  `;
  console.log();
  console.log(whatsNext);
  console.log();
  console.log(commands.map(command => `  ${command}`).join('\n'));
  const followUp = stripIndent`
    Quilt can also help you build, test, lint, and type-check your new application.
    You can learn more about building apps with Quilt by reading the documentation:
    ${underline(magenta('https://github.com/lemonmade/quilt/tree/main/documentation'))}

    Have fun! 🎉
  `;
  console.log();
  console.log(followUp);
}

// Argument handling

function getArgv() {
  const argv = parseArguments({
    '--yes': Boolean,
    '-y': '--yes',
    '--name': String,
    '--template': String,
    '--directory': String,
    '--install': Boolean,
    '--no-install': Boolean,
    '--monorepo': Boolean,
    '--no-monorepo': Boolean,
    '--package-manager': String,
    '--extras': [String],
    '--no-extras': Boolean,
    '--help': Boolean,
    '-h': '--help'
  }, {
    permissive: true
  });
  return argv;
}
async function getName(argv, {
  inWorkspace
}) {
  let {
    '--name': name
  } = argv;
  if (name == null) {
    name = await prompt({
      type: 'text',
      message: 'What would you like to name your new app?',
      initial: inWorkspace ? 'app' : 'my-quilt-app'
    });
  }
  return name;
}
async function getDirectory(argv, {
  name
}) {
  let directory = path.resolve(argv['--directory'] ?? toValidPackageName(name));
  while (!argv['--yes']) {
    if (fs.existsSync(directory) && !(await isEmpty(directory))) {
      const relativeDirectory = path.relative(process.cwd(), directory);
      const empty = await prompt({
        type: 'confirm',
        message: `Directory ${bold(relativeDirectoryForDisplay(relativeDirectory))} is not empty, is it safe to empty it?`,
        initial: true
      });
      if (empty) break;
      const promptDirectory = await prompt({
        type: 'text',
        message: 'What directory do you want to create your new app in?'
      });
      directory = path.resolve(promptDirectory);
    } else {
      break;
    }
  }
  return directory;
}
const VALID_TEMPLATES = new Set(['basic', 'graphql', 'trpc', 'empty']);
async function getTemplate(argv) {
  if (argv['--template'] && VALID_TEMPLATES.has(argv['--template'])) {
    return argv['--template'];
  }
  const template = await prompt({
    type: 'select',
    message: 'What template would you like to use?',
    hint: `Use ${bold('arrow keys')} to select, and ${bold('return')} to submit`,
    choices: [{
      title: `${bold('Empty')}, a basic React app without any extra runtime dependencies`,
      value: 'empty'
    }, {
      title: `${bold('The basics')}, a web app with a minimal file structure`,
      value: 'basic'
    }, {
      title: `${bold('GraphQL')}, a web app with a GraphQL API, fetched using @tanstack/react-query`,
      value: 'graphql'
    }, {
      title: `${bold('tRPC')}, a web app with a tRPC API, fetched using @tanstack/react-query`,
      value: 'trpc'
    }]
  });
  return template;
}

export { createApp };
