/* eslint-disable max-classes-per-file -- the smoke contract keeps its schemas, errors, and transitions together. */
import { Effect, Schema } from "effect";

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

const SmokePhase = Schema.Literals([
  "preflight",
  "planning",
  "worker-running",
  "reviewing",
  "completed",
  "failed",
]);
export type SmokePhase = typeof SmokePhase.Type;

const SmokeTransition = Schema.Literals([
  "preflight-passed",
  "plan-validated",
  "worker-succeeded",
  "review-approved",
  "contract-failed",
  "runtime-failed",
]);
type SmokeTransition = typeof SmokeTransition.Type;

export const SmokeRole = Schema.Literals(["planner-reviewer", "worker"]);
export type SmokeRole = typeof SmokeRole.Type;

const SmokeToolCallMode = Schema.Literal("serial");

export const SmokeRoleSelectionInput = Schema.Struct({
  plannerReviewerModel: MeaningfulString,
  plannerReviewerProvider: MeaningfulString,
  workerModel: MeaningfulString,
  workerProvider: MeaningfulString,
  workerToolCallMode: SmokeToolCallMode,
});
export type SmokeRoleSelectionInput = typeof SmokeRoleSelectionInput.Type;

export const SmokeModelRole = Schema.Struct({
  model: MeaningfulString,
  provider: MeaningfulString,
  role: SmokeRole,
  toolCallMode: Schema.Literal("serial"),
});
export type SmokeModelRole = typeof SmokeModelRole.Type;

export const SmokeModelRoles = Schema.Struct({
  plannerReviewer: SmokeModelRole,
  worker: SmokeModelRole,
});
export type SmokeModelRoles = typeof SmokeModelRoles.Type;

export const SmokeContractReason = Schema.Literals([
  "InvalidInput",
  "InvalidModelRole",
  "InvalidTransition",
  "InvalidModelResponse",
  "InvalidReport",
]);
export type SmokeContractReason = typeof SmokeContractReason.Type;

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

export const selectSmokeModelRoles = Effect.fn("Smoke.selectModelRoles")(
  function* selectSmokeModelRoles(input: unknown) {
    const selection: SmokeRoleSelectionInput =
      yield* Schema.decodeUnknownEffect(SmokeRoleSelectionInput)(input).pipe(
        Effect.mapError(
          (cause) =>
            new SmokeContractError({
              message: cause.message,
              reason: "InvalidInput",
            })
        )
      );

    if (selection.workerModel !== "minimax-m3") {
      return yield* Effect.fail(
        new SmokeContractError({
          message: "The smoke worker must use minimax-m3",
          reason: "InvalidModelRole",
        })
      );
    }

    if (selection.plannerReviewerModel !== "glm-5.2") {
      return yield* Effect.fail(
        new SmokeContractError({
          message: "The smoke planner/reviewer must use glm-5.2",
          reason: "InvalidModelRole",
        })
      );
    }

    return {
      plannerReviewer: {
        model: selection.plannerReviewerModel,
        provider: selection.plannerReviewerProvider,
        role: "planner-reviewer" as const,
        toolCallMode: "serial" as const,
      },
      worker: {
        model: selection.workerModel,
        provider: selection.workerProvider,
        role: "worker" as const,
        toolCallMode: "serial" as const,
      },
    } satisfies SmokeModelRoles;
  }
);

export const SmokePlan = Schema.Struct({
  acceptanceCriteria: Schema.NonEmptyArray(MeaningfulString),
  nonGoals: Schema.Array(MeaningfulString),
  steps: Schema.NonEmptyArray(MeaningfulString),
  summary: MeaningfulString,
});
export type SmokePlan = typeof SmokePlan.Type;

export const SmokeWorkerReport = Schema.Struct({
  status: Schema.Literals(["completed", "blocked", "failed"]),
  summary: MeaningfulString,
});
export type SmokeWorkerReport = typeof SmokeWorkerReport.Type;

export const SmokeReview = Schema.Struct({
  approved: Schema.Boolean,
  findings: Schema.Array(MeaningfulString),
  summary: MeaningfulString,
});
export type SmokeReview = typeof SmokeReview.Type;

export const SmokeChatResponse = Schema.Struct({
  choices: Schema.NonEmptyArray(
    Schema.Struct({
      message: Schema.Struct({ content: Schema.String }),
    })
  ),
});

export const SmokeModelCatalog = Schema.Struct({
  data: Schema.Array(Schema.Struct({ id: MeaningfulString })),
});
export type SmokeModelCatalog = typeof SmokeModelCatalog.Type;

export const decodeSmokeModelCatalog = Effect.fn("Smoke.decodeModelCatalog")(
  function* decodeSmokeModelCatalog(input: unknown) {
    return yield* Schema.decodeUnknownEffect(SmokeModelCatalog)(input).pipe(
      Effect.mapError(
        (cause) =>
          new SmokeContractError({
            message: cause.message,
            reason: "InvalidModelResponse",
          })
      )
    );
  }
);

export const decodeSmokeChatText = Effect.fn("Smoke.decodeChatText")(
  function* decodeSmokeChatText(input: unknown) {
    const response = yield* Schema.decodeUnknownEffect(SmokeChatResponse)(
      input
    ).pipe(
      Effect.mapError(
        (cause) =>
          new SmokeContractError({
            message: cause.message,
            reason: "InvalidModelResponse",
          })
      )
    );
    const text = response.choices[0].message.content;
    if (text.trim().length === 0) {
      return yield* Effect.fail(
        new SmokeContractError({
          message: "The model returned an empty response",
          reason: "InvalidModelResponse",
        })
      );
    }
    return text;
  }
);

export const decodeSmokePlan = Effect.fn("Smoke.decodePlan")(
  function* decodeSmokePlan(input: unknown) {
    return yield* Schema.decodeUnknownEffect(SmokePlan)(input).pipe(
      Effect.mapError(
        (cause) =>
          new SmokeContractError({
            message: cause.message,
            reason: "InvalidReport",
          })
      )
    );
  }
);

export const decodeSmokeWorkerReport = Effect.fn("Smoke.decodeWorkerReport")(
  function* decodeSmokeWorkerReport(input: unknown) {
    return yield* Schema.decodeUnknownEffect(SmokeWorkerReport)(input).pipe(
      Effect.mapError(
        (cause) =>
          new SmokeContractError({
            message: cause.message,
            reason: "InvalidReport",
          })
      )
    );
  }
);

export const decodeSmokeReview = Effect.fn("Smoke.decodeReview")(
  function* decodeSmokeReview(input: unknown) {
    return yield* Schema.decodeUnknownEffect(SmokeReview)(input).pipe(
      Effect.mapError(
        (cause) =>
          new SmokeContractError({
            message: cause.message,
            reason: "InvalidReport",
          })
      )
    );
  }
);

export const SmokeCapabilityCheck = Schema.Struct({
  id: MeaningfulString,
  message: MeaningfulString,
  passed: Schema.Boolean,
});
export type SmokeCapabilityCheck = typeof SmokeCapabilityCheck.Type;

export const validateSmokeCapabilities = Effect.fn(
  "Smoke.validateCapabilities"
)(function* validateSmokeCapabilities(input: unknown) {
  const checks = yield* Schema.decodeUnknownEffect(
    Schema.NonEmptyArray(SmokeCapabilityCheck)
  )(input).pipe(
    Effect.mapError(
      (cause) =>
        new SmokeContractError({
          message: cause.message,
          reason: "InvalidInput",
        })
    )
  );
  const failed = checks.filter((check) => !check.passed);
  if (failed.length > 0) {
    return yield* Effect.fail(
      new SmokeContractError({
        message: failed
          .map((check) => `${check.id}: ${check.message}`)
          .join("; "),
        reason: "InvalidReport",
      })
    );
  }
  return checks;
});

export const SmokeLoopState = Schema.Struct({
  history: Schema.Array(SmokePhase),
  lastError: Schema.NullOr(Schema.String),
  phase: SmokePhase,
});
export type SmokeLoopState = typeof SmokeLoopState.Type;

export const initialSmokeLoopState = (): SmokeLoopState => ({
  history: ["preflight"],
  lastError: null,
  phase: "preflight",
});

const nextPhase: Readonly<
  Record<SmokePhase, Partial<Record<SmokeTransition, SmokePhase>>>
> = {
  completed: {},
  failed: {},
  planning: {
    "plan-validated": "worker-running",
    "runtime-failed": "failed",
  },
  preflight: {
    "contract-failed": "failed",
    "preflight-passed": "planning",
    "runtime-failed": "failed",
  },
  reviewing: {
    "contract-failed": "failed",
    "review-approved": "completed",
    "runtime-failed": "failed",
  },
  "worker-running": {
    "contract-failed": "failed",
    "runtime-failed": "failed",
    "worker-succeeded": "reviewing",
  },
};

export const advanceSmokeLoop = Effect.fn("Smoke.advanceLoop")(
  function* advanceSmokeLoop(
    state: SmokeLoopState,
    transition: SmokeTransition,
    error?: string
  ) {
    const phase = nextPhase[state.phase][transition];
    if (phase === undefined) {
      return yield* Effect.fail(
        new SmokeContractError({
          message: `Cannot apply ${transition} while smoke loop is ${state.phase}`,
          reason: "InvalidTransition",
        })
      );
    }
    return {
      history: [...state.history, phase],
      lastError: phase === "failed" ? (error ?? "Smoke loop failed") : null,
      phase,
    } satisfies SmokeLoopState;
  }
);

export const redactSmokeText = (
  text: string,
  secrets: readonly string[]
): string => {
  let redacted = text;
  for (const secret of secrets) {
    if (secret.length > 0) {
      redacted = redacted.replaceAll(secret, "[REDACTED]");
    }
  }
  return redacted;
};
