/* eslint-disable max-classes-per-file -- each domain failure has a distinct tagged reason. */
import { Effect, Schema } from "effect";

import { ProblemStatement, ProjectId, SignalId } from "./signal.js";

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

export const ProjectIssueId = MeaningfulString.pipe(
  Schema.brand("ProjectIssueId")
);
export type ProjectIssueId = typeof ProjectIssueId.Type;

export const ProjectIssueStatus = Schema.Literals([
  "open",
  "queued",
  "working",
  "needs-input",
  "completed",
  "failed",
]);
export type ProjectIssueStatus = typeof ProjectIssueStatus.Type;

const ProjectIssueDraftInput = Schema.Struct({
  body: Schema.String,
  title: Schema.String,
});

export const ProjectIssueDraft = Schema.Struct({
  body: MeaningfulString,
  title: MeaningfulString,
});
export type ProjectIssueDraft = typeof ProjectIssueDraft.Type;

export const ExplicitProjectIssueRequest = Schema.Struct({
  body: Schema.String,
  kind: Schema.Literal("explicit"),
  projectId: ProjectId,
  title: Schema.String,
});

export const SignalProjectIssueRequest = Schema.Struct({
  kind: Schema.Literal("signal"),
  signalId: SignalId,
});

export const ProjectIssueRequest = Schema.Union([
  ExplicitProjectIssueRequest,
  SignalProjectIssueRequest,
]);
export type ProjectIssueRequest = typeof ProjectIssueRequest.Type;

export const ProjectIssueDispatchInput = Schema.Struct({
  issueId: ProjectIssueId,
  kind: Schema.Literal("project.issue.started"),
  projectId: ProjectId,
});
export type ProjectIssueDispatchInput = typeof ProjectIssueDispatchInput.Type;

export const ProjectIssueRequestResult = Schema.Struct({
  acceptedAt: MeaningfulString,
  dispatchId: MeaningfulString,
  issueId: ProjectIssueId,
  projectId: ProjectId,
  status: Schema.Literals(["queued", "working"]),
});
export type ProjectIssueRequestResult = typeof ProjectIssueRequestResult.Type;

export const ProjectIssueValidationReason = Schema.Literals([
  "InvalidInput",
  "TitleTooShort",
  "TitleTooLong",
  "BodyTooShort",
  "BodyTooLong",
]);
export type ProjectIssueValidationReason =
  typeof ProjectIssueValidationReason.Type;

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

export const ProjectIssueRequestErrorReason = Schema.Literals([
  "InvalidInput",
  "InvalidProjectId",
  "InvalidSignalId",
]);
export type ProjectIssueRequestErrorReason =
  typeof ProjectIssueRequestErrorReason.Type;

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

export const ProjectIssueTransitionReason = Schema.Literals([
  "InvalidStatus",
  "CompletedIssue",
]);
export type ProjectIssueTransitionReason =
  typeof ProjectIssueTransitionReason.Type;

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

export const validateProjectIssueDraft = (
  input: unknown
): Effect.Effect<ProjectIssueDraft, ProjectIssueValidationError> =>
  Effect.gen(function* validateDraft() {
    const decoded = yield* Schema.decodeUnknownEffect(ProjectIssueDraftInput)(
      input
    ).pipe(
      Effect.mapError(
        () =>
          new ProjectIssueValidationError({
            message: "Issue title and body are required",
            reason: "InvalidInput",
          })
      )
    );
    const title = decoded.title.trim();
    const body = decoded.body.trim();

    if (title.length < 3) {
      return yield* Effect.fail(
        new ProjectIssueValidationError({
          message: "Issue title must be between 3 and 160 characters",
          reason: "TitleTooShort",
        })
      );
    }
    if (title.length > 160) {
      return yield* Effect.fail(
        new ProjectIssueValidationError({
          message: "Issue title must be between 3 and 160 characters",
          reason: "TitleTooLong",
        })
      );
    }
    if (body.length < 10) {
      return yield* Effect.fail(
        new ProjectIssueValidationError({
          message: "Issue description must be between 10 and 10000 characters",
          reason: "BodyTooShort",
        })
      );
    }
    if (body.length > 10_000) {
      return yield* Effect.fail(
        new ProjectIssueValidationError({
          message: "Issue description must be between 10 and 10000 characters",
          reason: "BodyTooLong",
        })
      );
    }

    return { body, title };
  });

export const decodeProjectIssueRequest = (
  input: unknown
): Effect.Effect<
  ProjectIssueRequest,
  ProjectIssueRequestError | ProjectIssueValidationError
> =>
  Effect.gen(function* decodeRequest() {
    const request = yield* Schema.decodeUnknownEffect(ProjectIssueRequest)(
      input
    ).pipe(
      Effect.mapError(
        () =>
          new ProjectIssueRequestError({
            message:
              "Use a signal request or an explicit request with projectId, title, and body",
            reason: "InvalidInput",
          })
      )
    );

    if (request.kind === "explicit") {
      const draft = yield* validateProjectIssueDraft(request);
      return { ...request, ...draft };
    }

    return request;
  });

export const projectIssueDraftFromSignal = (input: unknown) =>
  Effect.gen(function* draftFromSignal() {
    const source = yield* Schema.decodeUnknownEffect(
      Schema.Struct({
        problemStatement: ProblemStatement,
        signalId: SignalId,
      })
    )(input).pipe(
      Effect.mapError(
        () =>
          new ProjectIssueRequestError({
            message: "Project Signal is missing a valid problem statement",
            reason: "InvalidSignalId",
          })
      )
    );

    const constraints =
      source.problemStatement.constraints.length === 0
        ? "None"
        : source.problemStatement.constraints
            .map((constraint) => `- ${constraint}`)
            .join("\n");
    return yield* validateProjectIssueDraft({
      body: [
        source.problemStatement.summary,
        `Desired outcome: ${source.problemStatement.desiredOutcome}`,
        `Constraints:\n${constraints}`,
        `Source Signal: ${source.signalId}`,
      ].join("\n\n"),
      title: source.problemStatement.title,
    });
  });

export const queueProjectIssue = (
  input: unknown
): Effect.Effect<"queued" | "working", ProjectIssueTransitionError> =>
  Effect.gen(function* queueIssue() {
    const status = yield* Schema.decodeUnknownEffect(ProjectIssueStatus)(
      input
    ).pipe(
      Effect.mapError(
        () =>
          new ProjectIssueTransitionError({
            message: "Issue has an invalid status",
            reason: "InvalidStatus",
          })
      )
    );

    if (status === "queued" || status === "working") {
      return status;
    }
    if (status === "completed") {
      return yield* Effect.fail(
        new ProjectIssueTransitionError({
          message: "Completed issues cannot be queued again",
          reason: "CompletedIssue",
        })
      );
    }
    return "queued" as const;
  });
