/* eslint-disable max-classes-per-file -- workspace errors stay next to their contracts. */
import { Effect, Schema } from "effect";

import { CONTEXT_KINDS } from "./project.js";

export type { ContextKind } from "./project.js";

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

const PositiveInteger = Schema.Int.check(Schema.isGreaterThanOrEqualTo(1));

const WORKSPACE_PATH = "/workspace";
const CHECKOUT_PATH = "/workspace/repository";
const CONTROL_PATH = "/workspace/control";

const isSafeRelativePath = (value: string): boolean => {
  if (value.length === 0 || value.startsWith("/") || value.includes("\\")) {
    return false;
  }
  return !value
    .split("/")
    .some((segment) => segment === "" || segment === "." || segment === "..");
};

const isValidGitRef = (value: string): boolean =>
  value.length > 0 &&
  !value.startsWith("/") &&
  !value.endsWith("/") &&
  !value.startsWith(".") &&
  !value.endsWith(".") &&
  !value.includes("..") &&
  !value.includes("//") &&
  !value.includes("@{") &&
  ![...value].some((character) => character < " ") &&
  !/[ ~^:?*[\\]/u.test(value);

const isPublicGitUrl = (value: string): boolean => {
  try {
    const url = new URL(value);
    return (
      (url.protocol === "http:" || url.protocol === "https:") &&
      url.hostname.length > 0 &&
      url.pathname !== "/" &&
      url.username.length === 0 &&
      url.password.length === 0 &&
      url.search.length === 0 &&
      url.hash.length === 0
    );
  } catch {
    return false;
  }
};

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

const slugIdentity = (value: string): string => {
  const parts = value
    .toLowerCase()
    .split(/[^a-z0-9]+/gu)
    .filter(Boolean);
  let result = "";
  for (const part of parts) {
    result = part;
  }
  return result.replaceAll(/^-+|-+$/gu, "").slice(-24) || "issue";
};

export const WorkspaceContextFile = Schema.Struct({
  content: Schema.String,
  kind: Schema.Literals([...CONTEXT_KINDS]),
  path: MeaningfulString,
});
export type WorkspaceContextFile = typeof WorkspaceContextFile.Type;

export const WorkspaceArtifactFile = Schema.Struct({
  content: Schema.String,
  path: MeaningfulString,
});
export type WorkspaceArtifactFile = typeof WorkspaceArtifactFile.Type;

export const IssueWorkspaceInput = Schema.Struct({
  artifacts: Schema.Array(WorkspaceArtifactFile),
  branchName: Schema.UndefinedOr(MeaningfulString),
  checkoutPath: Schema.UndefinedOr(MeaningfulString),
  contextFiles: Schema.Array(WorkspaceContextFile),
  defaultBranch: Schema.UndefinedOr(MeaningfulString),
  issueBody: MeaningfulString,
  issueId: MeaningfulString,
  issueNumber: PositiveInteger,
  issueTitle: MeaningfulString,
  sourceUrl: Schema.UndefinedOr(MeaningfulString),
});
export type IssueWorkspaceInput = typeof IssueWorkspaceInput.Type;

export const WorkspaceExecOperation = Schema.Struct({
  _tag: Schema.Literal("Exec"),
  command: MeaningfulString,
  cwd: MeaningfulString,
  timeoutMs: PositiveInteger,
});

export const WorkspaceMkdirOperation = Schema.Struct({
  _tag: Schema.Literal("Mkdir"),
  path: MeaningfulString,
});

export const WorkspaceWriteFileOperation = Schema.Struct({
  _tag: Schema.Literal("WriteFile"),
  content: Schema.String,
  path: MeaningfulString,
});

export const IssueWorkspaceOperation = Schema.Union([
  WorkspaceExecOperation,
  WorkspaceMkdirOperation,
  WorkspaceWriteFileOperation,
]);
export type IssueWorkspaceOperation = typeof IssueWorkspaceOperation.Type;

export const IssueWorkspacePlan = Schema.Struct({
  artifactsRoot: MeaningfulString,
  baseBranch: MeaningfulString,
  branchName: MeaningfulString,
  checkoutPath: MeaningfulString,
  contextRoot: MeaningfulString,
  issuePath: MeaningfulString,
  operations: Schema.Array(IssueWorkspaceOperation),
  sourceUrl: MeaningfulString,
});
export type IssueWorkspacePlan = typeof IssueWorkspacePlan.Type;

export const IssueWorkspaceCommandResult = Schema.Struct({
  exitCode: Schema.Int,
  stderr: Schema.String,
  stdout: Schema.String,
});
export type IssueWorkspaceCommandResult =
  typeof IssueWorkspaceCommandResult.Type;

export const IssueWorkspaceResult = Schema.Struct({
  baseBranch: MeaningfulString,
  branchName: MeaningfulString,
  checkoutPath: MeaningfulString,
  headSha: MeaningfulString,
  sourceUrl: MeaningfulString,
});
export type IssueWorkspaceResult = typeof IssueWorkspaceResult.Type;

export const WorkspaceValidationReason = Schema.Literals([
  "MissingSource",
  "InvalidInput",
  "InvalidSourceUrl",
  "InvalidBranchName",
  "InvalidPath",
  "DuplicatePath",
]);
export type WorkspaceValidationReason = typeof WorkspaceValidationReason.Type;

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

export const WorkspaceExecutionReason = Schema.Literals([
  "CommandFailed",
  "InvalidResult",
]);
export type WorkspaceExecutionReason = typeof WorkspaceExecutionReason.Type;

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

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

export class WorkspaceStateError extends Schema.TaggedErrorClass<WorkspaceStateError>()(
  "WorkspaceStateError",
  {
    from: WorkspaceRunStatus,
    message: Schema.String,
    to: WorkspaceRunStatus,
  }
) {}

const validationError = (message: string, reason: WorkspaceValidationReason) =>
  new WorkspaceValidationError({ message, reason });

const requireWorkspaceSource = (sourceUrl: string | undefined) => {
  if (!sourceUrl || sourceUrl.trim().length === 0) {
    return Effect.fail(
      validationError("Project source is not connected", "MissingSource")
    );
  }
  if (!isPublicGitUrl(sourceUrl)) {
    return Effect.fail(
      validationError(
        "Issue workspace source must be a public http(s) Git URL",
        "InvalidSourceUrl"
      )
    );
  }
  return Effect.succeed(sourceUrl);
};

const validateFiles = (
  files: readonly { readonly path: string }[],
  root: string
): Effect.Effect<void, WorkspaceValidationError> =>
  Effect.gen(function* validateWorkspaceFiles() {
    const paths = new Set<string>();
    for (const file of files) {
      if (!isSafeRelativePath(file.path)) {
        return yield* Effect.fail(
          validationError(
            `Invalid workspace file path: ${file.path}`,
            "InvalidPath"
          )
        );
      }
      const path = `${root}/${file.path}`;
      if (paths.has(path)) {
        return yield* Effect.fail(
          validationError(
            `Duplicate workspace file path: ${path}`,
            "DuplicatePath"
          )
        );
      }
      paths.add(path);
    }
  });

const makeBranchName = (issueNumber: number, issueId: string): string =>
  `work/issue-${issueNumber}-${slugIdentity(issueId)}`;

const makeCheckoutCommand = (input: {
  readonly baseBranch: string;
  readonly branchName: string;
  readonly checkoutPath: string;
  readonly sourceUrl: string;
}): string => {
  const checkout = shellQuote(input.checkoutPath);
  const baseBranch = shellQuote(input.baseBranch);
  const branch = shellQuote(input.branchName);
  const originBaseBranch = shellQuote(`origin/${input.baseBranch}`);
  const source = shellQuote(input.sourceUrl);

  return [
    "set -eu",
    `mkdir -p ${shellQuote(WORKSPACE_PATH)}`,
    `if [ ! -d ${checkout}/.git ]; then git clone --branch ${baseBranch} --single-branch ${source} ${checkout}; fi`,
    `if [ "$(git -C ${checkout} branch --show-current)" != ${branch} ]; then git -C ${checkout} show-ref --verify --quiet refs/heads/${branch} && git -C ${checkout} checkout ${branch} || git -C ${checkout} checkout -b ${branch} ${originBaseBranch}; fi`,
    `printf '%s\\n' "$(git -C ${checkout} branch --show-current)" "$(git -C ${checkout} rev-parse HEAD)"`,
  ].join(" && ");
};

export const makeIssueWorkspacePlan = Effect.fn("ProjectWorkspace.makePlan")(
  function* makeIssueWorkspacePlan(rawInput: unknown) {
    const input: IssueWorkspaceInput = yield* Schema.decodeUnknownEffect(
      IssueWorkspaceInput
    )(rawInput).pipe(
      Effect.mapError(() =>
        validationError("Invalid issue workspace input", "InvalidInput")
      )
    );

    const sourceUrl = yield* requireWorkspaceSource(input.sourceUrl);

    const baseBranch = input.defaultBranch?.trim() || "main";
    const branchName =
      input.branchName?.trim() ||
      makeBranchName(input.issueNumber, input.issueId);
    const checkoutPath = input.checkoutPath?.trim() || CHECKOUT_PATH;

    if (!isValidGitRef(baseBranch) || !isValidGitRef(branchName)) {
      return yield* Effect.fail(
        validationError(
          "Issue workspace branch name is not a valid Git ref",
          "InvalidBranchName"
        )
      );
    }
    if (
      checkoutPath !== CHECKOUT_PATH ||
      !checkoutPath.startsWith(`${WORKSPACE_PATH}/`) ||
      checkoutPath.includes("..")
    ) {
      return yield* Effect.fail(
        validationError(
          "Issue workspace checkout path must be /workspace/repository",
          "InvalidPath"
        )
      );
    }

    const contextRoot = `${CONTROL_PATH}/context`;
    const artifactsRoot = `${CONTROL_PATH}/artifacts`;
    yield* validateFiles(input.contextFiles, contextRoot);
    yield* validateFiles(input.artifacts, artifactsRoot);

    const operations: IssueWorkspaceOperation[] = [
      { _tag: "Mkdir", path: CONTROL_PATH },
      {
        _tag: "Exec",
        command: makeCheckoutCommand({
          baseBranch,
          branchName,
          checkoutPath,
          sourceUrl,
        }),
        cwd: WORKSPACE_PATH,
        timeoutMs: 300_000,
      },
      { _tag: "Mkdir", path: contextRoot },
      { _tag: "Mkdir", path: artifactsRoot },
      {
        _tag: "WriteFile",
        content: `# Issue ${input.issueNumber}: ${input.issueTitle}\n\n${input.issueBody}\n`,
        path: `${CONTROL_PATH}/issue.md`,
      },
      ...input.contextFiles.map((file) => ({
        _tag: "WriteFile" as const,
        content: file.content,
        path: `${contextRoot}/${file.path}`,
      })),
      ...input.artifacts.map((file) => ({
        _tag: "WriteFile" as const,
        content: file.content,
        path: `${artifactsRoot}/${file.path}`,
      })),
    ];

    return {
      artifactsRoot,
      baseBranch,
      branchName,
      checkoutPath,
      contextRoot,
      issuePath: `${CONTROL_PATH}/issue.md`,
      operations,
      sourceUrl,
    };
  }
);

export const decodeIssueWorkspaceResult = Effect.fn(
  "ProjectWorkspace.decodeResult"
)(function* decodeIssueWorkspaceResult(input: {
  readonly command: IssueWorkspaceCommandResult;
  readonly plan: IssueWorkspacePlan;
}) {
  if (input.command.exitCode !== 0) {
    return yield* Effect.fail(
      new WorkspaceExecutionError({
        message:
          input.command.stderr || "Issue workspace checkout command failed",
        reason: "CommandFailed",
      })
    );
  }

  const [branchName, headSha] = input.command.stdout.trim().split(/\s+/u);
  if (
    branchName !== input.plan.branchName ||
    !headSha ||
    !/^[0-9a-f]{7,64}$/u.test(headSha)
  ) {
    return yield* Effect.fail(
      new WorkspaceExecutionError({
        message:
          "Issue workspace checkout command returned invalid branch metadata",
        reason: "InvalidResult",
      })
    );
  }

  return {
    baseBranch: input.plan.baseBranch,
    branchName,
    checkoutPath: input.plan.checkoutPath,
    headSha,
    sourceUrl: input.plan.sourceUrl,
  };
});

const allowedTransitions: Readonly<
  Record<WorkspaceRunStatus, readonly WorkspaceRunStatus[]>
> = {
  completed: [],
  failed: [],
  "needs-input": ["queued", "working", "failed"],
  queued: ["working", "completed", "failed"],
  ready: ["queued", "working", "failed"],
  working: ["needs-input", "completed", "failed"],
};

export const transitionWorkspaceRun = Effect.fn(
  "ProjectWorkspace.transitionRun"
)(function* transitionWorkspaceRun(input: {
  readonly from: WorkspaceRunStatus;
  readonly to: WorkspaceRunStatus;
}) {
  if (
    input.from === input.to ||
    allowedTransitions[input.from].includes(input.to)
  ) {
    return input.to;
  }
  return yield* Effect.fail(
    new WorkspaceStateError({
      from: input.from,
      message: `Cannot transition issue workspace run from ${input.from} to ${input.to}`,
      to: input.to,
    })
  );
});
