import spawn from "cross-spawn"
import chalk from "chalk"
import {readJSONSync, writeJson} from "fs-extra"
import {join} from "path"
import {AppValuesBuilder} from "./template-builders/app-values-builder"
import {Generator, GeneratorOptions, SourceRootType} from "../generator"
import {baseLogger, log} from "../utils/log"
import {fetchLatestVersionsFor} from "../utils/fetch-latest-version-for"
import {getBlitzDependencyVersion} from "../utils/get-blitz-dependency-version"
import fs from "fs-extra"

function assert(condition: any, message: string): asserts condition {
  if (!condition) throw new Error(message)
}

export interface AppTemplateValues {
  name: string
  safeNameSlug: string
  username: string | undefined
}

type TemplateConfig = {
  path: string
  skipForms?: boolean
  skipDatabase?: boolean
}

export interface AppGeneratorOptions extends GeneratorOptions {
  template: TemplateConfig
  appName: string
  useTs: boolean
  yarn: boolean
  pnpm?: boolean
  version: string
  skipInstall: boolean
  skipGit: boolean
  form: "finalform" | "hookform" | "formik"
  onPostInstall?: () => Promise<void>
}

type PkgManager = "npm" | "yarn" | "pnpm"

export class AppGenerator extends Generator<AppGeneratorOptions> {
  sourceRoot: SourceRootType = {type: "template", path: this.options.template.path}
  // Disable file-level prettier because we manually run prettier at the end
  prettierDisabled = true
  packageInstallSuccess: boolean = false

  filesToIgnore() {
    if (!this.options.useTs) {
      return ["tsconfig.json", "next-env.d.ts", "vitest-config.ts", "package.ts.json", "types.ts"]
    }
    return ["jsconfig.json", "package.js.json", "vitest.config.js"]
  }

  templateValuesBuilder = new AppValuesBuilder(this.fs)

  getTargetDirectory() {
    return ""
  }

  // eslint-disable-next-line require-await
  async preCommit() {
    this.fs.move(this.destinationPath("gitignore"), this.destinationPath(".gitignore"))
    this.fs.move(this.destinationPath("npmrc"), this.destinationPath(".npmrc"))
    this.fs.move(
      this.destinationPath(this.options.useTs ? "package.ts.json" : "package.js.json"),
      this.destinationPath("package.json"),
    )

    const rpcEndpointPath = `src/app/api/rpc/blitzrpcroute/route.${
      this.options.useTs ? "ts" : "js"
    }`
    if (this.fs.exists(rpcEndpointPath)) {
      this.fs.move(
        this.destinationPath(rpcEndpointPath),
        this.destinationPath(
          `src/app/api/rpc/[[...blitz]]/route.${this.options.useTs ? "ts" : "js"}`,
        ),
      )
    }

    if (this.options.template.path === "app") {
      const pathsToMove = [
        "src/app/auth/components/LoginForm.tsx",
        "src/app/auth/components/LogoutButton.tsx",
        "src/app/auth/components/SignupForm.tsx",
        "src/app/auth/components/ForgotPasswordForm.tsx",
        "src/app/auth/components/ResetPasswordForm.tsx",
        "src/app/auth/mutations/login.ts",
        "src/app/auth/mutations/logout.ts",
        "src/app/auth/mutations/signup.ts",
        "src/app/auth/mutations/forgotPassword.ts",
        "src/app/auth/mutations/resetPassword.ts",
        "src/app/auth/mutations/forgotPassword.test.ts",
        "src/app/auth/mutations/resetPassword.test.ts",
        "src/app/auth/mutations/changePassword.ts",
        "src/app/auth/login/page.tsx",
        "src/app/auth/signup/page.tsx",
        "src/app/auth/forgot-password/page.tsx",
        "src/app/auth/reset-password/page.tsx",
        "src/app/auth/layout.tsx",
        "src/app/auth/validations.ts",
      ]
      for (const path of pathsToMove) {
        this.fs.move(
          this.destinationPath(path),
          this.destinationPath(path.replace("auth/", "(auth)/")),
        )
      }
    }

    if (!this.options.template.skipForms) {
      this.updateForms()
    }
  }

  async postWrite() {
    const {pkgManager} = this
    let gitInitSuccessful
    if (!this.options.skipGit) {
      const initResult = spawn.sync("git", ["init"], {
        stdio: "ignore",
      })

      gitInitSuccessful = initResult.status === 0
      if (!gitInitSuccessful) {
        baseLogger().warn("Failed to run git init.")
        baseLogger().warn(
          "Find out more about how to install git here: https://git-scm.com/downloads.",
        )
      }
    }
    const pkgJsonLocation = join(this.destinationPath(), "package.json")
    const pkg = readJSONSync(pkgJsonLocation)

    console.log("") // New line needed
    const spinner = log.spinner(log.withBrand("Retrieving the freshest of dependencies")).start()

    const [
      {value: newDependencies, isFallback: dependenciesUsedFallback},
      {value: newDevDependencies, isFallback: devDependenciesUsedFallback},
      {value: blitzDependencyVersion},
    ] = await Promise.all([
      fetchLatestVersionsFor(pkg.dependencies),
      fetchLatestVersionsFor(pkg.devDependencies),
      getBlitzDependencyVersion(),
    ])

    pkg.dependencies = newDependencies
    pkg.devDependencies = newDevDependencies
    pkg.dependencies.blitz = blitzDependencyVersion
    pkg.dependencies["@blitzjs/next"] = blitzDependencyVersion
    pkg.dependencies["@blitzjs/rpc"] = blitzDependencyVersion
    pkg.dependencies["@blitzjs/auth"] = blitzDependencyVersion

    const fallbackUsed = dependenciesUsedFallback || devDependenciesUsedFallback

    await writeJson(pkgJsonLocation, pkg, {spaces: 2})

    if (!fallbackUsed && !this.options.skipInstall) {
      spinner.succeed()

      await new Promise<void>((resolve) => {
        const logFlag = pkgManager === "yarn" ? "--json" : "--loglevel=error"
        const cp = spawn(pkgManager, ["install", logFlag], {
          stdio: ["inherit", "pipe", "pipe"],
        })

        const getJSON = (data: string) => {
          try {
            return JSON.parse(data)
          } catch {
            return null
          }
        }

        const spinners: any[] = []

        if (pkgManager !== "yarn") {
          const spinner = log
            .spinner(log.withBrand("Installing those dependencies (this will take a few minutes)"))
            .start()
          spinners.push(spinner)
        }

        cp.stdout?.setEncoding("utf8")
        cp.stderr?.setEncoding("utf8")
        cp.stdout?.on("data", (data) => {
          if (pkgManager === "pnpm") {
            if (data.includes("ERR_PNPM_PEER_DEP_ISSUES  Unmet peer dependencies")) {
              spinners[spinners.length - 1]?.succeed()
            }
          }

          if (pkgManager === "yarn") {
            let json = getJSON(data)
            if (json && json.type === "step") {
              spinners[spinners.length - 1]?.succeed()
              const spinner = log.spinner(log.withBrand(json.data.message)).start()
              spinners.push(spinner)
            }
            if (json && json.type === "success") {
              spinners[spinners.length - 1]?.succeed()
            }
          }
        })
        cp.stderr?.on("data", (data) => {
          if (pkgManager === "yarn") {
            let json = getJSON(data)
            if (json && json.type === "error") {
              spinners[spinners.length - 1]?.fail()
              console.error(json.data)
            }
          } else {
            // Hide the annoying Prisma warning about not finding the schema file
            // because we generate the client ourselves
            if (!data.includes("schema.prisma")) {
              console.error(`\n${data}`)
            }
          }
        })
        cp.on("exit", (code) => {
          if (pkgManager !== "yarn" && spinners[spinners.length - 1].isSpinning) {
            if (code !== 0) spinners[spinners.length - 1].fail()
            else {
              spinners[spinners.length - 1].succeed()
              this.packageInstallSuccess = true
            }
          }
          resolve()
        })
      })

      await this.options.onPostInstall?.()

      const runLocalNodeCLI = (command: string) => {
        const {pkgManager} = this
        if (pkgManager === "yarn") {
          return spawn.sync("yarn", ["run", ...command.split(" ")])
        } else if (pkgManager === "pnpm") {
          return spawn.sync("pnpm", command.split(" "))
        } else {
          return spawn.sync("npx", command.split(" "))
        }
      }

      // Ensure the generated files are formatted with the installed prettier version
      if (this.packageInstallSuccess) {
        const formattingSpinner = log.spinner(log.withBrand("Formatting your code")).start()
        const prettierResult = runLocalNodeCLI("prettier --loglevel silent --write .")
        if (prettierResult.status !== 0) {
          formattingSpinner.fail(
            chalk.yellow.bold(
              "We had an error running Prettier, but don't worry your app will still run fine :)",
            ),
          )
        } else {
          formattingSpinner.succeed()
        }
      }
    } else {
      console.log("") // New line needed
      if (this.options.skipInstall) {
        spinner.succeed()
      } else {
        spinner.fail(
          chalk.red.bold(
            `We had some trouble connecting to the network, so we'll skip installing your dependencies right now. Make sure to run ${`${this.pkgManager} install`} once you're connected again.`,
          ),
        )
      }
    }

    if (!this.options.skipGit && gitInitSuccessful) {
      this.commitChanges()
    }
  }

  preventFileFromLogging(path: string): boolean {
    if (path.includes(".env")) return false
    if (path.includes("eslint")) return false

    const filename = path.split("/").pop() as string
    return path[0] === "." || filename[0] === "."
  }

  commitChanges() {
    const commitSpinner = log.spinner(log.withBrand("Committing your app")).start()
    const commands: Array<[string, string[], object]> = [
      ["git", ["add", "."], {stdio: "ignore"}],
      [
        "git",
        ["commit", "--no-verify", "-m", "Brand new Blitz app!"],
        {stdio: "ignore", timeout: 10000},
      ],
    ]
    for (let command of commands) {
      const result = spawn.sync(...command)
      if (result.status !== 0) {
        commitSpinner.fail(
          chalk.red.bold(
            `Failed to run command ${command[0]} with ${command[1].join(" ")} options.`,
          ),
        )
        return
      }
    }
    commitSpinner.succeed()
  }

  private updateForms() {
    const pkg = this.fs.readJSON(this.destinationPath("package.json")) as
      | Record<string, any>
      | undefined
    assert(pkg, "couldn't find package.json")

    const ext = this.options.useTs ? "tsx" : "js"

    const type = this.options.form
    switch (type) {
      case "finalform":
        pkg.dependencies["final-form"] = "4.x"
        pkg.dependencies["react-final-form"] = "6.x"
        break
      case "hookform":
        pkg.dependencies["react-hook-form"] = "7.x"
        pkg.dependencies["@hookform/resolvers"] = "2.x"
        pkg.dependencies["@hookform/error-message"] = "2.x"
        break
      case "formik":
        pkg.dependencies["formik"] = "2.x"
        break
    }
    if (this.options.template.path === "app") {
      this.fs.move(
        this.destinationPath(`_forms/${type}/Form.${ext}`),
        this.destinationPath(`src/app/components/Form.${ext}`),
      )
      this.fs.move(
        this.destinationPath(`_forms/${type}/LabeledTextField.${ext}`),
        this.destinationPath(`src/app/components/LabeledTextField.${ext}`),
      )
    } else {
      this.fs.move(
        this.destinationPath(`_forms/${type}/Form.${ext}`),
        this.destinationPath(`src/core/components/Form.${ext}`),
      )
      this.fs.move(
        this.destinationPath(`_forms/${type}/LabeledTextField.${ext}`),
        this.destinationPath(`src/core/components/LabeledTextField.${ext}`),
      )
    }

    this.fs.writeJSON(this.destinationPath("package.json"), pkg)
    this.fs.delete(this.destinationPath("_forms"))
  }

  private get pkgManager(): PkgManager {
    if (this.options.pnpm) {
      return "pnpm"
    } else if (this.options.yarn) {
      return "yarn"
    } else {
      return "npm"
    }
  }
}
