import {
  decideGitLifecycle,
  GitLifecycleError,
  inspectGitWorkspace,
  makeCommitMessage,
  validatePullRequestMetadata,
} from "@code/primitives/git";
import type {
  GitLifecycleDecisionResult,
  GitLifecycleResult,
  GitWorkspaceInspection,
} from "@code/primitives/git";
import { Effect } from "effect";

export interface GitCommandResult {
  readonly exitCode: number;
  readonly stderr: string;
  readonly stdout: string;
}

export interface GitCommandRunner {
  readonly run: (
    command: string,
    options?: {
      readonly cwd?: string;
      readonly env?: Record<string, string>;
    }
  ) => Promise<GitCommandResult>;
}

export interface GiteaRepository {
  readonly cloneUrl: string;
  readonly defaultBranch: string;
  readonly htmlUrl: string;
  readonly name: string;
  readonly sshUrl: string;
}

export interface GiteaPullRequest {
  readonly base: { readonly ref: string };
  readonly head: { readonly ref: string };
  readonly htmlUrl: string;
  readonly number: number;
  readonly state: "open" | "closed" | "merged";
}

export interface GiteaTransport {
  readonly createPullRequest: (input: {
    readonly base: string;
    readonly body: string;
    readonly head: string;
    readonly repositoryPath: string;
    readonly title: string;
  }) => Promise<GiteaPullRequest>;
  readonly getRepository: (repositoryPath: string) => Promise<GiteaRepository>;
}

export interface GiteaHttpTransportOptions {
  readonly baseUrl: string;
  readonly fetch?: (
    input: string | URL | Request,
    init?: RequestInit
  ) => Promise<Response>;
  readonly token: string;
}

const shellQuote = (value: string): string =>
  `'${value.replaceAll("'", `'"'"'`)}'`;

const providerFailure = (
  message: string,
  reason: "CommandFailed" | "RemoteRejected"
) => new GitLifecycleError({ message, reason });

const runChecked = async (
  runner: GitCommandRunner,
  command: string,
  options?: Parameters<GitCommandRunner["run"]>[1]
): Promise<string> => {
  let result: GitCommandResult;
  try {
    result = await runner.run(command, options);
  } catch (error) {
    throw new GitLifecycleError({
      message: `Git command could not run: ${
        error instanceof Error ? error.message : String(error)
      }`,
      reason: "CommandFailed",
    });
  }
  if (result.exitCode !== 0) {
    throw providerFailure(
      result.stderr.trim() || `Git command failed: ${command}`,
      command.startsWith("git push") ? "RemoteRejected" : "CommandFailed"
    );
  }
  return result.stdout;
};

const toHttpPath = (repositoryPath: string): string =>
  repositoryPath
    .split("/")
    .map((segment) => encodeURIComponent(segment))
    .join("/");

const requestJson = async <T>(
  options: GiteaHttpTransportOptions,
  path: string,
  init?: RequestInit
): Promise<T> => {
  let response: Response;
  try {
    response = await (options.fetch ?? globalThis.fetch)(
      `${options.baseUrl.replace(/\/$/u, "")}${path}`,
      {
        ...init,
        headers: {
          Accept: "application/json",
          Authorization: `token ${options.token}`,
          "Content-Type": "application/json",
          ...init?.headers,
        },
      }
    );
  } catch (error) {
    throw new GitLifecycleError({
      message: `Gitea request could not run: ${
        error instanceof Error ? error.message : String(error)
      }`,
      reason: "RequestFailed",
    });
  }
  if (!response.ok) {
    const detail = await response.text();
    throw new GitLifecycleError({
      message: `Gitea request failed (${response.status}): ${detail}`,
      reason:
        response.status === 401 || response.status === 403
          ? "Authentication"
          : "RequestFailed",
    });
  }
  return (await response.json()) as T;
};

export const createGiteaHttpTransport = (
  options: GiteaHttpTransportOptions
): GiteaTransport => ({
  async createPullRequest(input) {
    const response = await requestJson<{
      base: { ref: string };
      head: { ref: string };
      html_url: string;
      number: number;
      state: "open" | "closed" | "merged";
    }>(options, `/api/v1/repos/${toHttpPath(input.repositoryPath)}/pulls`, {
      body: JSON.stringify({
        base: input.base,
        body: input.body,
        head: input.head,
        title: input.title,
      }),
      method: "POST",
    });
    return {
      base: response.base,
      head: response.head,
      htmlUrl: response.html_url,
      number: response.number,
      state: response.state,
    };
  },
  async getRepository(repositoryPath) {
    const response = await requestJson<{
      clone_url: string;
      default_branch: string;
      html_url: string;
      name: string;
      ssh_url: string;
    }>(options, `/api/v1/repos/${toHttpPath(repositoryPath)}`);
    return {
      cloneUrl: response.clone_url,
      defaultBranch: response.default_branch,
      htmlUrl: response.html_url,
      name: response.name,
      sshUrl: response.ssh_url,
    };
  },
});

export interface RunPostRunGiteaLifecycleInput {
  readonly baseBranch: string;
  readonly body: string;
  readonly commitMessage?: string;
  readonly issueNumber: number;
  readonly issueTitle: string;
  readonly repositoryPath: string;
  readonly runner: GitCommandRunner;
  readonly title?: string;
  readonly transport: GiteaTransport;
  readonly verification: "passed" | "failed" | "not-run";
  readonly workspace: string;
}

const inspect = async (
  input: RunPostRunGiteaLifecycleInput
): Promise<GitWorkspaceInspection> => {
  const [branch, status, diff] = await Promise.all([
    runChecked(input.runner, "git branch --show-current", {
      cwd: input.workspace,
    }),
    runChecked(input.runner, "git status --porcelain=v1", {
      cwd: input.workspace,
    }),
    runChecked(input.runner, "git diff --no-ext-diff --binary", {
      cwd: input.workspace,
    }),
  ]);
  return await Effect.runPromise(
    inspectGitWorkspace({
      branch: branch.trim(),
      diff,
      status,
    })
  );
};

export const runPostRunGiteaLifecycle = async (
  input: RunPostRunGiteaLifecycleInput
): Promise<GitLifecycleResult> => {
  const inspection = await inspect(input);
  const initialDecision = (await Effect.runPromise(
    decideGitLifecycle({
      baseBranch: input.baseBranch,
      inspection,
      pushed: false,
      verification: input.verification,
    })
  )) as GitLifecycleDecisionResult;

  if (initialDecision.decision === "finish") {
    return {
      baseBranch: input.baseBranch as GitLifecycleResult["baseBranch"],
      branch: inspection.branch,
      commitSha: undefined,
      pullRequest: undefined,
      status: "no_changes",
    };
  }

  const repository = await input.transport.getRepository(input.repositoryPath);

  await runChecked(
    input.runner,
    `git add --all && git commit -m ${shellQuote(
      input.commitMessage ??
        makeCommitMessage(input.issueNumber, input.issueTitle)
    )}`,
    { cwd: input.workspace }
  );
  const commitOutput = await runChecked(input.runner, "git rev-parse HEAD", {
    cwd: input.workspace,
  });
  const commitSha = commitOutput.trim();
  const cleanStatus = await runChecked(
    input.runner,
    "git status --porcelain=v1",
    { cwd: input.workspace }
  );
  if (cleanStatus.trim().length > 0) {
    throw providerFailure(
      "Git workspace remained dirty after commit",
      "CommandFailed"
    );
  }

  await runChecked(
    input.runner,
    `git push ${shellQuote(repository.sshUrl)} HEAD:${shellQuote(inspection.branch)}`,
    { cwd: input.workspace }
  );

  const pullRequest = await input.transport.createPullRequest({
    base: input.baseBranch,
    body: input.body,
    head: inspection.branch,
    repositoryPath: input.repositoryPath,
    title: input.title ?? `Issue #${input.issueNumber}: ${input.issueTitle}`,
  });
  const metadata = await Effect.runPromise(
    validatePullRequestMetadata({
      baseBranch: pullRequest.base.ref,
      branch: pullRequest.head.ref,
      number: pullRequest.number,
      status: pullRequest.state,
      url: pullRequest.htmlUrl,
    })
  );

  return {
    baseBranch: input.baseBranch as GitLifecycleResult["baseBranch"],
    branch: inspection.branch,
    commitSha,
    pullRequest: metadata,
    status: "pull_request_open",
  };
};
