import { Octokit } from "@octokit/rest";
import { execSync } from "child_process";
import dotenv from "dotenv";
import fs, { readFileSync } from "fs";
import readlineSync from "readline-sync";
import path, { dirname } from "path";
import { fileURLToPath } from "url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

dotenv.config();

const args = new Set(process.argv.slice(2));
const isFrontend = args.has("--fe");
const noDeploy = args.has("--no-deploy");
const isBackend = args.has("--be");
const isDryRun = args.has("--dry");
const noSyncCheck = args.has("--no-sync-check");
const hotfix = args.has("--hotfix");
const previewFe = args.has("--preview-fe");

const PROJECT_ROOT = path.resolve(__dirname, "../../../");

const runCommand = (command, force) => {
  if (isDryRun && !force) {
    console.log(`[Dry Run] Command: ${command}`);
    return "[Dry Run] Command executed.";
  } else {
    try {
      const output = execSync(command, { stdio: "pipe" }).toString();
      return output;
    } catch (error) {
      console.error(`Error executing command ${command}`);
      console.error(error.output.toString());
      process.exit(1);
    }
  }
  return undefined;
};

const runProjectRootCommand = (command, force) => {
  if (isDryRun && !force) {
    console.log(`[Dry Run] Command: ${command}`);
    return "[Dry Run] Command executed.";
  } else {
    try {
      const output = execSync(`cd ${PROJECT_ROOT} && ${command}`, {
        stdio: "pipe",
      }).toString();
      return output;
    } catch (error) {
      console.error(`Error executing command ${command}`);
      console.error(error);
      process.exit(1);
    }
  }
  return undefined;
};

const checkBranchSync = () => {
  console.log("Checking if local branch is master...");
  const currentBranch = runProjectRootCommand(
    "git branch --show-current",
  ).trim();
  if (currentBranch !== "master") {
    console.error(
      "Local branch is not master. Please checkout the master branch.",
    );
    process.exit(1);
  }

  console.log("Checking if local master branch is in sync with origin...");

  if (noSyncCheck) {
    console.log("Skipping sync check.");
  } else if (isDryRun) {
    console.log("[Dry Run] Checking sync...");
  } else {
    try {
      // Fetch the latest changes from the remote repository
      runProjectRootCommand("git fetch origin");

      // Get the commit hashes of the local and remote master branches
      const localMaster = runProjectRootCommand("git rev-parse master").trim();
      const remoteMaster = runProjectRootCommand(
        "git rev-parse origin/master",
      ).trim();

      if (localMaster !== remoteMaster) {
        console.error(
          "Local master branch is not in sync with origin. Please pull the latest changes before proceeding.",
        );
        process.exit(1);
      }
    } catch (error) {
      console.error("Error checking branch sync status.");
      console.error(error);
      process.exit(1);
    }
  }
};

const getCurrentVersion = () => {
  console.log("Getting current version...");

  const rootPackageJson = JSON.parse(
    readFileSync(`${PROJECT_ROOT}/package.json`, "utf-8"),
  );

  return rootPackageJson.version;
};

const incrementVersion = (currentVersion) => {
  console.log("Incrementing version...");
  const now = new Date();
  const year = Number(now.getFullYear().toString().slice(-2));
  const start = new Date(now.getFullYear(), 0, 1);
  const week = Math.ceil(((now - start) / 86400000 + start.getDay() + 1) / 7);
  const [prevYear, prevWeek, minor] = currentVersion.split(".").map(Number);

  let newMinor = minor + 1;
  if (year !== prevYear || week !== prevWeek) {
    newMinor = 0;
  }

  const v = `v${year}.${week}.${newMinor}`;

  return v;
};

const updatePackage = (newVersion) => {
  console.log("Updating package.json...");
  if (isDryRun) {
    console.log(`[Dry Run] Updated package.json to version ${newVersion}`);
    return;
  }
  const packagePath = `${PROJECT_ROOT}/package.json`;

  // Read the package.json file
  const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8"));

  // Update the version field
  packageJson.version = newVersion.replace("v", "");

  // Write the updated JSON back to package.json
  fs.writeFileSync(
    packagePath,
    JSON.stringify(packageJson, null, 2) + "\n",
    "utf8",
  );

  console.log(`Updated package.json to version ${newVersion}`);
};

const checkUncommittedChanges = () => {
  console.log("Checking uncommitted changes...");
  const status = execSync("git status --porcelain").toString().trim();
  if (isDryRun) {
    console.log("[Dry Run] Checking uncommitted changes...");
  } else if (status) {
    console.error(
      "You have uncommitted changes. Please commit or stash them before proceeding.",
    );
    process.exit(1);
  }
};

const installDependencies = () => {
  console.log("Installing dependencies...");
  if (isDryRun) {
    console.log("[Dry Run] Dependencies would be installed.");
  } else {
    runProjectRootCommand("pnpm i");
  }
};

const buildProject = () => {
  console.log("Building project...");

  if (isFrontend && !isBackend) {
    runProjectRootCommand(
      "NODE_ENV=production SENTRY=1 npx turbo lint test check-assets build --filter @monkeytype/frontend --force",
    );
  } else if (isBackend && !isFrontend) {
    runProjectRootCommand(
      "NODE_ENV=production SENTRY=1 npx turbo lint test build --filter @monkeytype/backend --force",
    );
  } else {
    runProjectRootCommand(
      "NODE_ENV=production SENTRY=1 npx turbo lint test check-assets build --force",
    );
  }
};

const deployBackend = () => {
  console.log("Deploying backend...");
  const p = path.resolve(__dirname, "../bin/deployBackend.sh");
  runCommand(`sh ${p}`);
};

const deployFrontend = () => {
  console.log("Deploying frontend...");
  runProjectRootCommand(
    "cd frontend && npx firebase deploy -P live --only hosting",
  );
};

const purgeCache = () => {
  console.log("Purging Cloudflare cache...");
  const p = path.resolve(__dirname, "../bin/purgeCfCache.sh");
  runCommand(`sh ${p}`);
};

const generateChangelog = async () => {
  console.log("Generating changelog...");

  const p = path.resolve(__dirname, "./buildChangelog.js");

  const changelog = runCommand(`node ${p}`, true);

  return changelog;
};

const generateContributors = () => {
  console.log("Generating contributors list...");
  try {
    const p = path.resolve(__dirname, "./buildContributors.js");

    let contributors = runCommand(`node ${p}`, true);

    contributors = JSON.parse(
      contributors.replaceAll("\n", "").replace(/^.*?\[/, "["),
    );

    fs.writeFileSync(
      `${PROJECT_ROOT}/frontend/static/contributors.json`,
      JSON.stringify(contributors, null, 2),
      "utf8",
    );

    console.log("Contributors list updated.");
  } catch (e) {
    console.error("Failed to generate contributors list.");
    console.error(e);
  }
};

const createCommitAndTag = (version) => {
  console.log("Creating commit and tag... Pushing to Github...");
  runCommand(`git add .`);
  runCommand(`git commit -m "chore: release ${version}" --no-verify`);
  runCommand(`git tag ${version}`);
  runCommand(`git push origin master --tags --no-verify`);
};

const createGithubRelease = async (version, changelogContent) => {
  console.log("Creating GitHub release...");
  if (isDryRun) {
    console.log(
      `[Dry Run] Sent release request to GitHub for version ${version}`,
    );
  } else {
    const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
    const { owner, repo } = {
      owner: "monkeytypegame",
      repo: "monkeytype",
    };
    await octokit.repos.createRelease({
      owner,
      repo,
      tag_name: version,
      name: `${version}`,
      body: changelogContent,
    });
  }
};

const main = async () => {
  if (previewFe) {
    console.log(`Starting frontend preview deployment process...`);
    installDependencies();
    runProjectRootCommand(
      "NODE_ENV=production npx turbo lint test check-assets build --filter @monkeytype/frontend --force",
    );

    const name = readlineSync.question(
      "Enter preview channel name (default: preview): ",
    );
    let channelName = name.trim();

    if (channelName === "") {
      channelName = "preview";
    }

    const expirationTime = readlineSync.question(
      "Enter expiration time (e.g., 2h, default: 1d): ",
    );
    let expires = expirationTime.trim();

    if (expires === "") {
      expires = "1d";
    }

    console.log(
      `Deploying frontend preview to channel "${channelName}" with expiration "${expires}"...`,
    );
    const result = runProjectRootCommand(
      `cd frontend && npx firebase hosting:channel:deploy ${channelName} -P live --expires ${expires}`,
    );
    console.log(result);
    console.log("Frontend preview deployed successfully.");
    process.exit(0);
  }

  console.log(`Starting ${hotfix ? "hotfix" : "release"} process...`);

  if (!hotfix) checkBranchSync();

  checkUncommittedChanges();

  installDependencies();

  let changelogContent;
  let newVersion;
  if (!hotfix) {
    changelogContent = await generateChangelog();

    console.log(changelogContent);

    if (!readlineSync.keyInYN("Changelog looks good?")) {
      console.log("Exiting.");
      process.exit(1);
    }

    const currentVersion = getCurrentVersion();
    newVersion = incrementVersion(currentVersion);
    console.log(`New version: ${newVersion}`);
  }
  buildProject();

  if (!hotfix && !readlineSync.keyInYN(`Ready to release ${newVersion}?`)) {
    console.log("Exiting.");
    process.exit(1);
  }

  if (!noDeploy && (isBackend || (!isFrontend && !isBackend))) {
    deployBackend();
  }

  if (!noDeploy && (isFrontend || (!isFrontend && !isBackend))) {
    deployFrontend();
  }

  if (!noDeploy) purgeCache();
  if (!hotfix) {
    generateContributors();
    updatePackage(newVersion);
    createCommitAndTag(newVersion);
    try {
      await createGithubRelease(newVersion, changelogContent);
    } catch (e) {
      console.error(`Failed to create release on GitHub: ${e}`);
      console.log("Please create the release manually.");
    }
  }

  if (hotfix) {
    console.log("Hotfix completed successfully.");
  } else {
    console.log(`Release ${newVersion} completed successfully.`);
  }
  process.exit(0);
};

main();
