/* eslint-disable max-classes-per-file -- GiteaError and Gitea form one adapter contract. */
import { Context, Effect, Layer, Schema } from "effect";

const toPrState = (
  merged: boolean,
  state: string
): "merged" | "closed" | "open" => {
  if (merged) {
    return "merged";
  }
  if (state === "closed") {
    return "closed";
  }
  return "open";
};

// ---------------------------------------------------------------------------
// Schemas / errors
// ---------------------------------------------------------------------------

export class GiteaError extends Schema.TaggedErrorClass<GiteaError>()(
  "GiteaError",
  {
    message: Schema.String,
    status: Schema.Number.pipe(Schema.optional),
  }
) {}

export interface GiteaIssue {
  readonly number: number;
  readonly title: string;
  readonly body: string;
  readonly state: "open" | "closed";
  readonly htmlUrl: string;
}

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

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

export interface CreateIssueInput {
  readonly title: string;
  readonly body: string;
}

export interface EditIssueInput {
  readonly title?: string;
  readonly body?: string;
  readonly state?: "open" | "closed";
}

export interface CreatePrInput {
  readonly title: string;
  readonly body: string;
  readonly head: string;
  readonly base: string;
}

// ---------------------------------------------------------------------------
// Service interface
// ---------------------------------------------------------------------------

export interface GiteaService {
  readonly getIssue: (number: number) => Effect.Effect<GiteaIssue, GiteaError>;
  readonly createIssue: (
    input: CreateIssueInput
  ) => Effect.Effect<GiteaIssue, GiteaError>;
  readonly editIssue: (
    number: number,
    input: EditIssueInput
  ) => Effect.Effect<GiteaIssue, GiteaError>;
  readonly listPullRequests: () => Effect.Effect<
    GiteaPullRequest[],
    GiteaError
  >;
  readonly getPullRequest: (
    number: number
  ) => Effect.Effect<GiteaPullRequest, GiteaError>;
  readonly createPullRequest: (
    input: CreatePrInput
  ) => Effect.Effect<GiteaPullRequest, GiteaError>;
  readonly getRepository: () => Effect.Effect<GiteaRepository, GiteaError>;
}

export interface GiteaConfig {
  readonly baseUrl: string;
  readonly token: string;
  readonly repoPath: string;
}

// ---------------------------------------------------------------------------
// HTTP helper
// ---------------------------------------------------------------------------

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

const authHeaders = (token: string): Record<string, string> => {
  const headers: Record<string, string> = {
    Accept: "application/json",
    "Content-Type": "application/json",
  };
  if (token) {
    headers.Authorization = `token ${token}`;
  }
  return headers;
};

const giteaFetch = <T>(
  config: GiteaConfig,
  path: string,
  init?: RequestInit
): Effect.Effect<T, GiteaError> =>
  Effect.tryPromise({
    catch: (cause) => {
      if (
        cause instanceof Error &&
        "status" in cause &&
        typeof cause.status === "number"
      ) {
        return new GiteaError({
          message: cause.message,
          status: cause.status,
        });
      }
      return new GiteaError({
        message: cause instanceof Error ? cause.message : String(cause),
      });
    },
    try: async () => {
      const response = await fetch(
        `${config.baseUrl.replace(/\/$/u, "")}${path}`,
        {
          ...init,
          headers: { ...authHeaders(config.token), ...init?.headers },
        }
      );
      if (!response.ok) {
        const detail = await response.text();
        throw Object.assign(new Error(`Gitea ${response.status}: ${detail}`), {
          status: response.status,
        });
      }
      if (response.status === 204) {
        return undefined as T;
      }
      return (await response.json()) as T;
    },
  });

// ---------------------------------------------------------------------------
// Service + layer
// ---------------------------------------------------------------------------

export class Gitea extends Context.Service<Gitea, GiteaService>()(
  "@zopu/server/Gitea"
) {
  static readonly layer = (config: GiteaConfig) =>
    Layer.succeed(
      Gitea,
      Gitea.of({
        createIssue: (input) =>
          giteaFetch<{
            number: number;
            title: string;
            body: string;
            state: string;
            html_url: string;
          }>(config, `/api/v1/repos/${toHttpPath(config.repoPath)}/issues`, {
            body: JSON.stringify({ body: input.body, title: input.title }),
            method: "POST",
          }).pipe(
            Effect.map(
              (i): GiteaIssue => ({
                body: i.body ?? "",
                htmlUrl: i.html_url,
                number: i.number,
                state: "open",
                title: i.title,
              })
            )
          ),

        createPullRequest: (input) =>
          giteaFetch<{
            number: number;
            title: string;
            state: string;
            html_url: string;
            merged: boolean;
            base: { ref: string };
            head: { ref: string };
          }>(config, `/api/v1/repos/${toHttpPath(config.repoPath)}/pulls`, {
            body: JSON.stringify({
              base: input.base,
              body: input.body,
              head: input.head,
              title: input.title,
            }),
            method: "POST",
          }).pipe(
            Effect.map(
              (pr): GiteaPullRequest => ({
                base: pr.base.ref,
                head: pr.head.ref,
                htmlUrl: pr.html_url,
                merged: pr.merged ?? false,
                number: pr.number,
                state: "open",
                title: pr.title,
              })
            )
          ),

        editIssue: (number, input) =>
          giteaFetch<{
            number: number;
            title: string;
            body: string;
            state: string;
            html_url: string;
          }>(
            config,
            `/api/v1/repos/${toHttpPath(config.repoPath)}/issues/${number}`,
            {
              body: JSON.stringify({
                ...(input.body === undefined ? {} : { body: input.body }),
                ...(input.state === undefined ? {} : { state: input.state }),
                ...(input.title === undefined ? {} : { title: input.title }),
              }),
              method: "PATCH",
            }
          ).pipe(
            Effect.map(
              (i): GiteaIssue => ({
                body: i.body ?? "",
                htmlUrl: i.html_url,
                number: i.number,
                state: i.state === "closed" ? "closed" : "open",
                title: i.title,
              })
            )
          ),

        getIssue: (number) =>
          giteaFetch<{
            number: number;
            title: string;
            body: string;
            state: string;
            html_url: string;
          }>(
            config,
            `/api/v1/repos/${toHttpPath(config.repoPath)}/issues/${number}`
          ).pipe(
            Effect.map(
              (i): GiteaIssue => ({
                body: i.body ?? "",
                htmlUrl: i.html_url,
                number: i.number,
                state: i.state === "closed" ? "closed" : "open",
                title: i.title,
              })
            )
          ),

        getPullRequest: (number) =>
          giteaFetch<{
            number: number;
            title: string;
            state: string;
            html_url: string;
            merged: boolean;
            base: { ref: string };
            head: { ref: string };
          }>(
            config,
            `/api/v1/repos/${toHttpPath(config.repoPath)}/pulls/${number}`
          ).pipe(
            Effect.map(
              (pr): GiteaPullRequest => ({
                base: pr.base.ref,
                head: pr.head.ref,
                htmlUrl: pr.html_url,
                merged: pr.merged ?? false,
                number: pr.number,
                state: toPrState(pr.merged, pr.state),
                title: pr.title,
              })
            )
          ),

        getRepository: () =>
          giteaFetch<{
            clone_url: string;
            ssh_url: string;
            html_url: string;
            default_branch: string;
            name: string;
          }>(config, `/api/v1/repos/${toHttpPath(config.repoPath)}`).pipe(
            Effect.map(
              (r): GiteaRepository => ({
                cloneUrl: r.clone_url,
                defaultBranch: r.default_branch,
                htmlUrl: r.html_url,
                name: r.name,
                sshUrl: r.ssh_url,
              })
            )
          ),

        listPullRequests: () =>
          giteaFetch<
            {
              number: number;
              title: string;
              state: string;
              html_url: string;
              merged: boolean;
              base: { ref: string };
              head: { ref: string };
            }[]
          >(
            config,
            `/api/v1/repos/${toHttpPath(config.repoPath)}/pulls?state=all&limit=20&sort=recentupdate`
          ).pipe(
            Effect.map((prs): GiteaPullRequest[] =>
              prs.map(
                (pr): GiteaPullRequest => ({
                  base: pr.base.ref,
                  head: pr.head.ref,
                  htmlUrl: pr.html_url,
                  merged: pr.merged ?? false,
                  number: pr.number,
                  state: toPrState(pr.merged, pr.state),
                  title: pr.title,
                })
              )
            )
          ),
      })
    );
}
