import { regex } from "arkregex";
import { type } from "arktype";
import { log } from "../utils/cli.ts";
import { $git } from "../utils/gitAuth.ts";
import { $ } from "../utils/shell.ts";
import type { ToolContext } from "./server.ts";
import { execute, tool } from "./shared.ts";

type PushDestination = {
  remoteName: string;
  remoteBranch: string;
  url: string;
};

/**
 * get where git would actually push this branch.
 * reads branch.X.pushRemote and branch.X.merge set by checkout_pr.
 *
 * NOTE: we read git config directly instead of using @{push} because
 * push.default=simple (git's default) requires local/remote branch names
 * to match. since checkout_pr uses pr-N as the local name, @{push} resolves
 * to origin/pr-N instead of the actual remote branch.
 *
 * for branches created via checkout_pr: uses configured pushRemote/merge
 * for new branches (git checkout -b): falls back to origin/<branch>
 */
function getPushDestination(branch: string): PushDestination {
  try {
    const pushRemote = $("git", ["config", `branch.${branch}.pushRemote`], { log: false }).trim();
    const merge = $("git", ["config", `branch.${branch}.merge`], { log: false }).trim();
    const remoteBranch = merge.replace(/^refs\/heads\//, "");
    const url = $("git", ["remote", "get-url", "--push", pushRemote], { log: false }).trim();
    return { remoteName: pushRemote, remoteBranch, url };
  } catch {
    // no push config - branch was created locally without checkout_pr
    log.debug(`no push config for ${branch}, falling back to origin/${branch}`);
    const url = $("git", ["remote", "get-url", "--push", "origin"], { log: false }).trim();
    return { remoteName: "origin", remoteBranch: branch, url };
  }
}

/**
 * normalize URL for comparison (handle .git suffix, case)
 */
function normalizeUrl(url: string): string {
  return url.replace(/\.git$/, "").toLowerCase();
}

type ValidatePushParams = {
  branch: string;
  pushUrl: string;
};

/**
 * validate that the push destination matches expected URL.
 * pushUrl is set by setupGit (base repo) and updated by checkout_pr (fork repo).
 */
function validatePushDestination(params: ValidatePushParams): PushDestination {
  const dest = getPushDestination(params.branch);

  if (normalizeUrl(dest.url) !== normalizeUrl(params.pushUrl)) {
    throw new Error(
      `Push blocked: destination does not match expected repository.\n` +
        `Expected: ${params.pushUrl}\n` +
        `Actual: ${dest.url}\n` +
        `Git configuration may have been tampered with.`
    );
  }

  return dest;
}

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) {
  const defaultBranch = ctx.repo.data.default_branch || "main";
  const pushPermission = ctx.payload.push;

  return tool({
    name: "push_branch",
    description:
      "Push the current branch to the remote repository. Omit branchName to push the current branch (recommended). " +
      "If specifying branchName, use the LOCAL branch name (e.g., 'pr-1'), not the remote branch name. " +
      "The correct remote and remote branch are determined automatically from branch config set by checkout_pr. " +
      "Never force push unless explicitly requested. Pushes to the default branch are blocked in restricted mode.",
    parameters: PushBranch,
    execute: execute(async ({ branchName, force }) => {
      // permission check
      if (pushPermission === "disabled") {
        throw new Error("Push is disabled. This repository is configured for read-only access.");
      }

      const branch = branchName || $("git", ["rev-parse", "--abbrev-ref", "HEAD"], { log: false });

      // validate push destination matches expected URL
      const pushUrl = ctx.toolState.pushUrl;
      if (!pushUrl) {
        throw new Error("pushUrl not set - setupGit must run before push_branch");
      }
      const pushDest = validatePushDestination({ branch, pushUrl });

      // block pushes to default branch in restricted mode
      if (pushPermission === "restricted" && pushDest.remoteBranch === defaultBranch) {
        throw new Error(
          `Push blocked: cannot push directly to default branch '${pushDest.remoteBranch}'. ` +
            `Create a feature branch and open a PR instead.`
        );
      }

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

      log.debug(`pushing ${branch} to ${pushDest.remoteName}/${pushDest.remoteBranch}`);
      if (force) {
        log.warning(`force pushing - this will overwrite remote history`);
      }
      $git("push", pushArgs, {
        token: ctx.gitToken,
        restricted: ctx.payload.bash !== "enabled",
      });

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

// commands that require authentication - redirect to dedicated tools
const AUTH_REQUIRED_REDIRECT: Record<string, string> = {
  push: "Use push_branch tool instead.",
  fetch: "Use git_fetch tool instead.",
  pull: "Use git_fetch + git merge instead.",
  clone: "Repository already cloned. Use checkout_pr for PR branches.",
};

// SECURITY: subcommands blocked when bash is disabled.
// in disabled mode the agent has NO shell access, so these subcommands are the
// primary escape vectors for arbitrary code execution. in restricted mode the
// agent already has bash in a stripped sandbox, so blocking these is redundant.
const NOBASH_BLOCKED_SUBCOMMANDS: Record<string, string> = {
  config: "Blocked: git config can set up filter drivers or hooks that execute arbitrary code.",
  submodule:
    "Blocked: git submodule can reference malicious repositories and execute code on update.",
  "update-index":
    "Blocked: git update-index can modify index entries in ways that bypass file protections.",
  "filter-branch": "Blocked: git filter-branch executes arbitrary code on repository history.",
  replace: "Blocked: git replace can redirect object lookups.",
  // subcommands that accept --exec or similar flags for arbitrary code execution
  rebase: "Blocked: git rebase --exec can execute arbitrary shell commands.",
  bisect: "Blocked: git bisect run can execute arbitrary shell commands.",
};

// SECURITY: subcommand-specific arg flags that execute code.
// only blocked when bash is disabled — in restricted mode the agent already
// has shell access in a stripped sandbox, so these provide no additional security.
//
// NOTE: global git flags like -c and --config-env are NOT included here
// because they only work before the subcommand. in the MCP tool, the
// subcommand is always first, so -c in args is parsed as a subcommand flag
// (e.g., git log -c = combined diff format), not config injection.
// the subcommand check (rejecting "-" prefix) already blocks that attack.
//
// matched as: arg === flag OR arg starts with flag + "="
// (avoids false positives like --exclude matching --exec)
const NOBASH_BLOCKED_ARGS = ["--exec", "--extcmd", "--upload-pack", "--receive-pack"];

// SECURITY: subcommand must match [a-z][a-z0-9-]* to reject flags passed as the subcommand.
// this blocks injection of global git options like -c, -C, --exec-path, --config-env, etc.
//
// critical attack: git -c "alias.x=!evil-command" x
//   -> sets alias "x" to a shell command via -c config injection, then runs it
//   -> achieves arbitrary code execution even with bash=disabled
const subcommandPattern = regex("^[a-z][a-z0-9-]*$");

const Git = type({
  subcommand: type(subcommandPattern).describe("Git subcommand (e.g., 'status', 'log', 'diff')"),
  args: type.string.array().describe("Additional arguments for the git command").optional(),
});

export function GitTool(ctx: ToolContext) {
  return tool({
    name: "git",
    description:
      "Run git commands. For push/fetch/pull, use the dedicated MCP tools instead (push_branch, git_fetch).",
    parameters: Git,
    execute: execute(async (params) => {
      const subcommand = params.subcommand;
      const args = params.args ?? [];

      const redirect = AUTH_REQUIRED_REDIRECT[subcommand];
      if (redirect) {
        throw new Error(`git ${subcommand} requires authentication. ${redirect}`);
      }

      // SECURITY: block dangerous subcommands when bash is disabled.
      // in restricted mode the agent has bash in a stripped sandbox, so blocking
      // these through the MCP tool is redundant (agent can do it via bash).
      if (ctx.payload.bash === "disabled") {
        const blocked = NOBASH_BLOCKED_SUBCOMMANDS[subcommand];
        if (blocked) {
          throw new Error(blocked);
        }

        // block subcommand-specific flags that execute arbitrary code
        for (const arg of args) {
          const isBlocked = NOBASH_BLOCKED_ARGS.some(
            (flag) => arg === flag || arg.startsWith(flag + "=")
          );
          if (isBlocked) {
            throw new Error(
              `Blocked: '${arg}' flag can execute arbitrary code and is not allowed.`
            );
          }
        }
      }

      const output = $("git", [subcommand, ...args]);
      return { success: true, output };
    }),
  });
}

const GitFetch = type({
  ref: type.string.describe("Ref to fetch: branch name, tag, or 'pull/N/head' for PRs"),
  depth: type.number.describe("Fetch depth (for shallow clones)").optional(),
});

export function GitFetchTool(ctx: ToolContext) {
  return tool({
    name: "git_fetch",
    description: "Fetch refs from remote repository. Use this instead of git fetch directly.",
    parameters: GitFetch,
    execute: execute(async (params) => {
      const fetchArgs = ["--no-tags", "origin", params.ref];
      if (params.depth !== undefined) {
        fetchArgs.push(`--depth=${params.depth}`);
      }
      $git("fetch", fetchArgs, {
        token: ctx.gitToken,
        restricted: ctx.payload.bash !== "enabled",
      });
      return { success: true, ref: params.ref };
    }),
  });
}

const DeleteBranch = type({
  branchName: type.string.describe("Remote branch to delete"),
});

export function DeleteBranchTool(ctx: ToolContext) {
  const pushPermission = ctx.payload.push;

  return tool({
    name: "delete_branch",
    description: "Delete a remote branch. Requires push: enabled permission.",
    parameters: DeleteBranch,
    execute: execute(async (params) => {
      if (pushPermission !== "enabled") {
        throw new Error(
          "Branch deletion requires push: enabled permission. " +
            "Current mode only allows pushing to non-protected branches."
        );
      }

      $git("push", ["origin", "--delete", params.branchName], {
        token: ctx.gitToken,
        restricted: ctx.payload.bash !== "enabled",
      });
      return { success: true, deleted: params.branchName };
    }),
  });
}

const PushTags = type({
  tag: type.string.describe("Tag name to push"),
  force: type.boolean.describe("Force push the tag").default(false),
});

export function PushTagsTool(ctx: ToolContext) {
  const pushPermission = ctx.payload.push;

  return tool({
    name: "push_tags",
    description: "Push a tag to remote. Requires push: enabled permission.",
    parameters: PushTags,
    execute: execute(async (params) => {
      if (pushPermission !== "enabled") {
        throw new Error(
          "Tag pushing requires push: enabled permission. " +
            "Current mode only allows pushing branches."
        );
      }

      const pushArgs = [...(params.force ? ["-f"] : []), "origin", `refs/tags/${params.tag}`];
      $git("push", pushArgs, {
        token: ctx.gitToken,
        restricted: ctx.payload.bash !== "enabled",
      });
      return { success: true, tag: params.tag };
    }),
  });
}
