/* eslint-disable max-classes-per-file -- the runtime service and its tagged error form one adapter contract. */
import { Context, Effect, Layer, Schema } from "effect";

const MeaningfulString = Schema.String.check(
  Schema.makeFilter((value) => value.trim().length > 0, {
    expected: "a non-empty string",
  })
);

// ---------------------------------------------------------------------------
// Configuration
// ---------------------------------------------------------------------------

export const GitRemoteConfig = Schema.Struct({
  baseUrl: Schema.String,
  owner: MeaningfulString,
  repo: MeaningfulString,
  token: MeaningfulString,
});
export type GitRemoteConfig = typeof GitRemoteConfig.Type;

// ---------------------------------------------------------------------------
// Value objects
// ---------------------------------------------------------------------------

export const RemoteIssue = Schema.Struct({
  body: Schema.String,
  number: Schema.Int.check(Schema.isGreaterThan(0)),
  state: Schema.Literals(["open", "closed"]),
  title: MeaningfulString,
  url: Schema.String,
});
export type RemoteIssue = typeof RemoteIssue.Type;

export const RemoteBranch = Schema.Struct({
  name: MeaningfulString,
});
export type RemoteBranch = typeof RemoteBranch.Type;

export const RemotePullRequest = Schema.Struct({
  baseBranch: MeaningfulString,
  branch: MeaningfulString,
  number: Schema.Int.check(Schema.isGreaterThan(0)),
  state: Schema.Literals(["open", "closed", "merged"]),
  title: MeaningfulString,
  url: Schema.String,
});
export type RemotePullRequest = typeof RemotePullRequest.Type;

// ---------------------------------------------------------------------------
// Errors
// ---------------------------------------------------------------------------

export const GitRemoteErrorReason = Schema.Literals([
  "InvalidInput",
  "Unauthorized",
  "NotFound",
  "Conflict",
  "Unreachable",
]);
export type GitRemoteErrorReason = typeof GitRemoteErrorReason.Type;

export class GitRemoteError extends Schema.TaggedErrorClass<GitRemoteError>()(
  "GitRemoteError",
  {
    message: Schema.String,
    reason: GitRemoteErrorReason,
  }
) {}

// ---------------------------------------------------------------------------
// Low-level transport: a tiny fetch wrapper that maps HTTP failures to the
// tagged error. Kept as an injectable dependency so tests can stub the wire.
// ---------------------------------------------------------------------------

export interface GitRemoteTransport {
  readonly request: (input: {
    readonly body?: unknown;
    readonly headers?: Readonly<Record<string, string>>;
    readonly method: string;
    readonly url: string;
  }) => Effect.Effect<
    { readonly status: number; readonly json: () => Promise<unknown> },
    GitRemoteError
  >;
}

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

const endpoint = (config: GitRemoteConfig, path: string): string =>
  `${config.baseUrl.replace(/\/+$/u, "")}/api/v1/repos/${config.owner}/${config.repo}${path}`;

const fromStatus = (status: number, fallback: string): GitRemoteError => {
  if (status === 401 || status === 403) {
    return new GitRemoteError({
      message: "Unauthorized",
      reason: "Unauthorized",
    });
  }
  if (status === 404) {
    return new GitRemoteError({ message: "Not found", reason: "NotFound" });
  }
  if (status === 409) {
    return new GitRemoteError({ message: "Conflict", reason: "Conflict" });
  }
  if (status >= 500) {
    return new GitRemoteError({ message: fallback, reason: "Unreachable" });
  }
  return new GitRemoteError({ message: fallback, reason: "InvalidInput" });
};

const request = (
  transport: GitRemoteTransport,
  config: GitRemoteConfig,
  method: string,
  path: string,
  body?: unknown
): Effect.Effect<unknown, GitRemoteError> =>
  transport
    .request({
      body,
      headers: authHeaders(config.token),
      method,
      url: endpoint(config, path),
    })
    .pipe(
      Effect.flatMap((res) =>
        Effect.gen(function* handleResponse() {
          if (res.status >= 400) {
            const detail = yield* Effect.tryPromise({
              catch: () => null,
              try: () => res.json(),
            }).pipe(Effect.orElseSucceed(() => null));
            const message =
              detail && typeof detail === "object" && "message" in detail
                ? String((detail as { message: unknown }).message)
                : `Request ${method} ${path} failed with ${res.status}`;
            return yield* Effect.fail(fromStatus(res.status, message));
          }
          return yield* Effect.tryPromise({
            catch: (cause) =>
              new GitRemoteError({
                message: cause instanceof Error ? cause.message : "Bad JSON",
                reason: "InvalidInput",
              }),
            try: () => res.json(),
          });
        })
      )
    );

// ---------------------------------------------------------------------------
// Operations
// ---------------------------------------------------------------------------

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

export const createIssue = (
  transport: GitRemoteTransport,
  config: GitRemoteConfig,
  input: CreateIssueInput
): Effect.Effect<RemoteIssue, GitRemoteError> =>
  Effect.gen(function* run() {
    const raw = yield* request(transport, config, "POST", "/issues", {
      body: input.body,
      title: input.title,
    });
    return yield* Schema.decodeUnknownEffect(RemoteIssue)({
      body: (raw as { body?: string }).body ?? "",
      number: (raw as { number: number }).number,
      state: (raw as { state: string }).state ?? "open",
      title: (raw as { title: string }).title,
      url: (raw as { html_url?: string }).html_url ?? "",
    }).pipe(
      Effect.mapError(
        (cause) =>
          new GitRemoteError({ message: cause.message, reason: "InvalidInput" })
      )
    );
  });

export interface CreateBranchInput {
  readonly from?: string;
  readonly name: string;
}

export const createBranch = (
  transport: GitRemoteTransport,
  config: GitRemoteConfig,
  input: CreateBranchInput
): Effect.Effect<RemoteBranch, GitRemoteError> =>
  Effect.gen(function* run() {
    const raw = yield* request(transport, config, "POST", "/branches", {
      new_branch_name: input.name,
      old_ref_name: input.from ?? "main",
    });
    return yield* Schema.decodeUnknownEffect(RemoteBranch)({
      name: (raw as { name: string }).name,
    }).pipe(
      Effect.mapError(
        (cause) =>
          new GitRemoteError({ message: cause.message, reason: "InvalidInput" })
      )
    );
  });

export interface CreatePullRequestInput {
  readonly baseBranch: string;
  readonly body: string;
  readonly branch: string;
  readonly title: string;
}

export const createPullRequest = (
  transport: GitRemoteTransport,
  config: GitRemoteConfig,
  input: CreatePullRequestInput
): Effect.Effect<RemotePullRequest, GitRemoteError> =>
  Effect.gen(function* run() {
    const raw = yield* request(transport, config, "POST", "/pulls", {
      base: input.baseBranch,
      body: input.body,
      head: input.branch,
      title: input.title,
    });
    const merged = Boolean((raw as { merged?: boolean }).merged);
    return yield* Schema.decodeUnknownEffect(RemotePullRequest)({
      baseBranch:
        (raw as { base?: { ref?: string } }).base?.ref ?? input.baseBranch,
      branch: (raw as { head?: { ref?: string } }).head?.ref ?? input.branch,
      number: (raw as { number: number }).number,
      state: merged ? "merged" : ((raw as { state?: string }).state ?? "open"),
      title: (raw as { title: string }).title,
      url: (raw as { html_url?: string }).html_url ?? "",
    }).pipe(
      Effect.mapError(
        (cause) =>
          new GitRemoteError({ message: cause.message, reason: "InvalidInput" })
      )
    );
  });

export const listIssues = (
  transport: GitRemoteTransport,
  config: GitRemoteConfig
): Effect.Effect<readonly RemoteIssue[], GitRemoteError> =>
  Effect.gen(function* run() {
    const raw = yield* request(
      transport,
      config,
      "GET",
      "/issues?state=open&type=issues"
    );
    const list = (raw as Record<string, unknown>[]) ?? [];
    const decoded = list.map((entry) =>
      Schema.decodeUnknownEffect(RemoteIssue)({
        body: (entry.body as string) ?? "",
        number: entry.number as number,
        state: (entry.state as string) ?? "open",
        title: entry.title as string,
        url: (entry.html_url as string) ?? "",
      }).pipe(
        Effect.mapError(
          (cause) =>
            new GitRemoteError({
              message: cause.message,
              reason: "InvalidInput",
            })
        )
      )
    );
    return yield* Effect.all(decoded);
  });

export const getIssue = (
  transport: GitRemoteTransport,
  config: GitRemoteConfig,
  number: number
): Effect.Effect<RemoteIssue, GitRemoteError> =>
  Effect.gen(function* run() {
    const raw = yield* request(transport, config, "GET", `/issues/${number}`);
    return yield* Schema.decodeUnknownEffect(RemoteIssue)({
      body: (raw as { body?: string }).body ?? "",
      number: (raw as { number: number }).number,
      state: (raw as { state: string }).state ?? "open",
      title: (raw as { title: string }).title,
      url: (raw as { html_url?: string }).html_url ?? "",
    }).pipe(
      Effect.mapError(
        (cause) =>
          new GitRemoteError({ message: cause.message, reason: "InvalidInput" })
      )
    );
  });

// ---------------------------------------------------------------------------
// Fetch-backed transport (default) + service
// ---------------------------------------------------------------------------

export const fetchTransport: GitRemoteTransport = {
  request: ({ body, headers, method, url }) =>
    Effect.tryPromise({
      catch: (cause) =>
        new GitRemoteError({
          message: cause instanceof Error ? cause.message : "Network error",
          reason: "Unreachable",
        }),
      try: async () => {
        const init: RequestInit = { headers: headers ?? {}, method };
        if (body !== undefined && method !== "GET" && method !== "HEAD") {
          init.body = JSON.stringify(body);
        }
        const res = await fetch(url, init);
        return {
          json: () => res.json() as Promise<unknown>,
          status: res.status,
        };
      },
    }),
};

interface GitRemoteRuntimeShape {
  readonly config: GitRemoteConfig;
  readonly transport: GitRemoteTransport;
  readonly createIssue: (
    input: CreateIssueInput
  ) => Effect.Effect<RemoteIssue, GitRemoteError>;
  readonly createBranch: (
    input: CreateBranchInput
  ) => Effect.Effect<RemoteBranch, GitRemoteError>;
  readonly createPullRequest: (
    input: CreatePullRequestInput
  ) => Effect.Effect<RemotePullRequest, GitRemoteError>;
  readonly listIssues: () => Effect.Effect<
    readonly RemoteIssue[],
    GitRemoteError
  >;
  readonly getIssue: (
    number: number
  ) => Effect.Effect<RemoteIssue, GitRemoteError>;
}

export class GitRemoteRuntime extends Context.Service<
  GitRemoteRuntime,
  GitRemoteRuntimeShape
>()("@code/primitives/git-remote-runtime/GitRemoteRuntime") {}

export const makeGitRemoteRuntimeLayer = (config: GitRemoteConfig) =>
  Layer.succeed(
    GitRemoteRuntime,
    GitRemoteRuntime.of({
      config,
      createBranch: (input) => createBranch(fetchTransport, config, input),
      createIssue: (input) => createIssue(fetchTransport, config, input),
      createPullRequest: (input) =>
        createPullRequest(fetchTransport, config, input),
      getIssue: (number) => getIssue(fetchTransport, config, number),
      listIssues: () => listIssues(fetchTransport, config),
      transport: fetchTransport,
    })
  );
