import { Effect, Schema } from "effect";

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

export const GitBranchName = MeaningfulString.pipe(
  Schema.check(
    Schema.makeFilter(
      (value) =>
        !value.startsWith("-") &&
        !value.includes("..") &&
        !value.includes(" ") &&
        !value.includes("~") &&
        !value.includes("^") &&
        !value.includes(":") &&
        !value.includes("\\"),
      { expected: "a valid Git branch name" }
    )
  )
);
export type GitBranchName = typeof GitBranchName.Type;

export const GitStatusEntry = Schema.Struct({
  index: Schema.String,
  path: MeaningfulString,
  raw: Schema.String,
  worktree: Schema.String,
});
export type GitStatusEntry = typeof GitStatusEntry.Type;

export const GitWorkspaceInspection = Schema.Struct({
  branch: GitBranchName,
  diff: Schema.String,
  hasChanges: Schema.Boolean,
  status: Schema.Array(GitStatusEntry),
});
export type GitWorkspaceInspection = typeof GitWorkspaceInspection.Type;

export const GitVerification = Schema.Literals(["passed", "failed", "not-run"]);
export type GitVerification = typeof GitVerification.Type;

export const PullRequestStatus = Schema.Literals(["open", "closed", "merged"]);
export type PullRequestStatus = typeof PullRequestStatus.Type;

export const PullRequestMetadata = Schema.Struct({
  baseBranch: GitBranchName,
  branch: GitBranchName,
  number: Schema.Int.check(Schema.isGreaterThan(0)),
  status: PullRequestStatus,
  url: Schema.String.check(
    Schema.makeFilter(
      (value) => {
        try {
          return new URL(value).protocol.startsWith("http");
        } catch {
          return false;
        }
      },
      { expected: "a valid HTTP pull request URL" }
    )
  ),
});
export type PullRequestMetadata = typeof PullRequestMetadata.Type;

export const GitLifecycleStatus = Schema.Literals([
  "no_changes",
  "committed",
  "pushed",
  "pull_request_open",
  "failed",
]);
export type GitLifecycleStatus = typeof GitLifecycleStatus.Type;

export const GitLifecycleResult = Schema.Struct({
  baseBranch: GitBranchName,
  branch: GitBranchName,
  commitSha: Schema.optional(MeaningfulString),
  pullRequest: Schema.optional(PullRequestMetadata),
  status: GitLifecycleStatus,
});
export type GitLifecycleResult = typeof GitLifecycleResult.Type;

export const GitLifecycleDecision = Schema.Literals([
  "finish",
  "commit",
  "push",
  "create_pull_request",
  "manual_merge",
]);
export type GitLifecycleDecision = typeof GitLifecycleDecision.Type;

export const GitLifecycleDecisionInput = Schema.Struct({
  baseBranch: GitBranchName,
  commitSha: Schema.optional(MeaningfulString),
  inspection: GitWorkspaceInspection,
  pullRequest: Schema.optional(PullRequestMetadata),
  pushed: Schema.Boolean,
  verification: GitVerification,
});
export type GitLifecycleDecisionInput = typeof GitLifecycleDecisionInput.Type;

export const GitLifecycleDecisionResult = Schema.Struct({
  decision: GitLifecycleDecision,
  status: GitLifecycleStatus,
});
export type GitLifecycleDecisionResult = typeof GitLifecycleDecisionResult.Type;

export const GitLifecycleErrorReason = Schema.Literals([
  "Authentication",
  "CommandFailed",
  "InvalidInput",
  "InvalidBranch",
  "NoChanges",
  "InvalidPullRequest",
  "RemoteRejected",
  "RequestFailed",
  "VerificationRequired",
]);
export type GitLifecycleErrorReason = typeof GitLifecycleErrorReason.Type;

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

const invalidInput = (message: string) =>
  new GitLifecycleError({ message, reason: "InvalidInput" });

export const parseGitStatus = (rawStatus: string): GitStatusEntry[] =>
  rawStatus
    .split("\n")
    .filter((line) => line.length > 0)
    .map((raw) => ({
      index: raw.slice(0, 1),
      path: raw.slice(3).trim(),
      raw,
      worktree: raw.slice(1, 2),
    }));

export const inspectGitWorkspace = Effect.fn("Git.inspectWorkspace")(
  function* inspectGitWorkspace(input: unknown) {
    const decoded = yield* Schema.decodeUnknownEffect(
      Schema.Struct({
        branch: GitBranchName,
        diff: Schema.String,
        status: Schema.String,
      })
    )(input).pipe(Effect.mapError((cause) => invalidInput(cause.message)));

    const status = parseGitStatus(decoded.status);
    return {
      branch: decoded.branch,
      diff: decoded.diff,
      hasChanges: status.length > 0 || decoded.diff.length > 0,
      status,
    } satisfies GitWorkspaceInspection;
  }
);

export const decideGitLifecycle = Effect.fn("Git.decideLifecycle")(
  function* decideGitLifecycle(input: unknown) {
    const decoded = yield* Schema.decodeUnknownEffect(
      GitLifecycleDecisionInput
    )(input).pipe(Effect.mapError((cause) => invalidInput(cause.message)));

    if (decoded.inspection.branch === decoded.baseBranch) {
      return yield* Effect.fail(
        new GitLifecycleError({
          message: `Refusing to publish the default branch ${decoded.baseBranch}`,
          reason: "InvalidBranch",
        })
      );
    }

    if (decoded.verification !== "passed") {
      return yield* Effect.fail(
        new GitLifecycleError({
          message: "Verified changes are required before publishing",
          reason: "VerificationRequired",
        })
      );
    }

    if (!decoded.inspection.hasChanges && decoded.commitSha === undefined) {
      return {
        decision: "finish" as const,
        status: "no_changes" as const,
      } satisfies GitLifecycleDecisionResult;
    }

    if (decoded.commitSha === undefined) {
      return {
        decision: "commit" as const,
        status: "committed" as const,
      } satisfies GitLifecycleDecisionResult;
    }

    if (!decoded.pushed) {
      return {
        decision: "push" as const,
        status: "pushed" as const,
      } satisfies GitLifecycleDecisionResult;
    }

    if (decoded.pullRequest === undefined) {
      return {
        decision: "create_pull_request" as const,
        status: "pull_request_open" as const,
      } satisfies GitLifecycleDecisionResult;
    }

    return {
      decision: "manual_merge" as const,
      status: "pull_request_open" as const,
    } satisfies GitLifecycleDecisionResult;
  }
);

export const validatePullRequestMetadata = Effect.fn(
  "Git.validatePullRequestMetadata"
)(function* validatePullRequestMetadata(input: unknown) {
  const metadata = yield* Schema.decodeUnknownEffect(PullRequestMetadata)(
    input
  ).pipe(Effect.mapError((cause) => invalidInput(cause.message)));

  if (metadata.branch === metadata.baseBranch) {
    return yield* Effect.fail(
      new GitLifecycleError({
        message: "A pull request must target a different branch",
        reason: "InvalidPullRequest",
      })
    );
  }

  return metadata;
});

export const makeCommitMessage = (
  issueNumber: number,
  title: string
): string => {
  const normalizedTitle = title.replaceAll(/\s+/gu, " ").trim();
  return `feat(issue-${issueNumber}): ${normalizedTitle}`.slice(0, 120);
};
