import { type } from "arktype";
import type { ToolContext } from "../main.ts";
import { log } from "../utils/cli.ts";
import { containsSecrets } from "../utils/secrets.ts";
import { $ } from "../utils/shell.ts";
import { execute, tool } from "./shared.ts";

export function CreateBranchTool(ctx: ToolContext) {
  const defaultBranch = ctx.repo.default_branch || "main";

  const CreateBranch = type({
    branchName: type.string.describe(
      "The name of the branch to create (e.g., 'pullfrog/123-fix-bug')"
    ),
    baseBranch: type.string
      .describe(`The base branch to create from (defaults to '${defaultBranch}')`)
      .default(defaultBranch),
  });

  return tool({
    name: "create_branch",
    description:
      "Create a new git branch from the specified base branch. The branch will be created locally and pushed to the remote repository.",
    parameters: CreateBranch,
    execute: execute(async ({ branchName, baseBranch }) => {
      // baseBranch should always be defined due to default, but TypeScript needs help
      const resolvedBaseBranch = baseBranch || ctx.repo.default_branch || "main";

      // validate branch name for secrets
      if (containsSecrets(branchName)) {
        throw new Error(
          "Branch creation blocked: secrets detected in branch name. " +
            "Please remove any sensitive information (API keys, tokens, passwords) before creating a branch."
        );
      }

      log.debug(`Creating branch ${branchName} from ${resolvedBaseBranch}`);

      // fetch base branch to ensure we're up to date
      $("git", ["fetch", "origin", resolvedBaseBranch, "--depth=1"]);

      // checkout base branch, ensuring it matches the remote version
      // -B creates or resets the branch to match origin/baseBranch
      $("git", ["checkout", "-B", resolvedBaseBranch, `origin/${resolvedBaseBranch}`]);

      // create and checkout new branch
      $("git", ["checkout", "-b", branchName]);

      // push branch to remote (set upstream)
      $("git", ["push", "-u", "origin", branchName]);

      log.debug(`Successfully created and pushed branch ${branchName}`);

      return {
        success: true,
        branchName,
        baseBranch: resolvedBaseBranch,
        message: `Branch ${branchName} created from ${resolvedBaseBranch} and pushed to remote`,
      };
    }),
  });
}

export const CommitFiles = type({
  message: type.string.describe("The commit message"),
  files: type.string
    .array()
    .describe(
      "Array of file paths to commit (relative to repo root). If empty, commits all staged changes."
    ),
});

export function CommitFilesTool(_ctx: ToolContext) {
  return tool({
    name: "commit_files",
    description:
      "Stage and commit files with a commit message. If files array is empty, commits all staged changes. The commit will be attributed to the correct bot account.",
    parameters: CommitFiles,
    execute: execute(async ({ message, files }) => {
      // validate commit message for secrets
      if (containsSecrets(message)) {
        throw new Error(
          "Commit blocked: secrets detected in commit message. " +
            "Please remove any sensitive information (API keys, tokens, passwords) before committing."
        );
      }

      // validate files for secrets if provided
      if (files.length > 0) {
        for (const file of files) {
          try {
            // try to read file content - if it exists, check for secrets
            const content = $("cat", [file], { log: false });
            if (containsSecrets(content)) {
              throw new Error(
                `Commit blocked: secrets detected in file ${file}. ` +
                  "Please remove any sensitive information (API keys, tokens, passwords) before committing."
              );
            }
          } catch (error) {
            // if error is about secrets, re-throw it
            if (error instanceof Error && error.message.includes("Commit blocked")) {
              throw error;
            }
            // if file doesn't exist (cat fails), that's ok - it will be created by git add
            // other errors are also ok - git add will handle them
          }
        }
      }

      const currentBranch = $("git", ["rev-parse", "--abbrev-ref", "HEAD"], { log: false });
      log.debug(`Committing files on branch ${currentBranch}`);

      // stage files if provided, otherwise stage all changes
      if (files.length > 0) {
        $("git", ["add", ...files]);
      } else {
        $("git", ["add", "."]);
      }

      // commit with message
      $("git", ["commit", "-m", message]);

      const commitSha = $("git", ["rev-parse", "HEAD"], { log: false });
      log.debug(`Successfully committed: ${commitSha.substring(0, 7)}`);

      return {
        success: true,
        commitSha,
        branch: currentBranch,
        message: `Committed ${files.length > 0 ? files.length + " file(s)" : "all changes"} with message: ${message}`,
      };
    }),
  });
}

export const PushBranch = type({
  branchName: type.string
    .describe("The branch name to push (defaults to current branch)")
    .optional(),
  force: type.boolean.describe("Force push (use with caution)").default(false),
});

export function PushBranchTool(_ctx: ToolContext) {
  return tool({
    name: "push_branch",
    description:
      "Push the current branch (or specified branch) to the remote repository. Git automatically determines the correct remote based on branch config (set by checkout_pr for fork PRs). Never force push unless explicitly requested.",
    parameters: PushBranch,
    execute: execute(async ({ branchName, force }) => {
      const branch = branchName || $("git", ["rev-parse", "--abbrev-ref", "HEAD"], { log: false });

      // check if branch has a configured pushRemote
      let remote = "origin";
      try {
        remote = $("git", ["config", `branch.${branch}.pushRemote`], { log: false }).trim();
      } catch {
        // no configured pushRemote, default to origin
      }

      // check if branch has a configured merge ref (remote branch name may differ from local)
      let remoteBranch = branch;
      try {
        const mergeRef = $("git", ["config", `branch.${branch}.merge`], { log: false }).trim();
        // merge ref is like "refs/heads/main", extract the branch name
        remoteBranch = mergeRef.replace("refs/heads/", "");
      } catch {
        // no configured merge ref, use local branch name
      }

      // use refspec when local and remote branch names differ
      const refspec = branch === remoteBranch ? branch : `${branch}:${remoteBranch}`;
      const args = force
        ? ["push", "--force", "-u", remote, refspec]
        : ["push", "-u", remote, refspec];

      log.debug(`pushing ${branch} to ${remote}/${remoteBranch}`);
      if (force) {
        log.warning(`force pushing - this will overwrite remote history`);
      }
      $("git", args);

      return {
        success: true,
        branch,
        remoteBranch,
        remote,
        force,
        message: `successfully pushed ${branch} to ${remote}/${remoteBranch}`,
      };
    }),
  });
}
