import {
  decodeProjectIssueRequest,
  ProjectIssueDispatchInput,
  ProjectIssueRequestError,
  ProjectIssueRequestResult,
  ProjectIssueValidationError,
} from "@code/primitives/project-issue";
import type { ProjectIssueRequest } from "@code/primitives/project-issue";
import { dispatch } from "@flue/runtime";
import type { ConvexHttpClient } from "convex/browser";
import { makeFunctionReference } from "convex/server";
import { Effect, Schema } from "effect";
import type { Context } from "hono";

import { createAuthenticatedClient, extractBearerToken } from "./auth";

interface ProjectIssueCreateArgs extends Record<string, unknown> {
  readonly body: string;
  readonly projectId: string;
  readonly title: string;
}

const createIssue = makeFunctionReference<
  "mutation",
  ProjectIssueCreateArgs,
  string
>("projectIssues:create");

const createIssueFromSignal = makeFunctionReference<
  "mutation",
  { readonly signalId: string },
  { readonly issueId: string; readonly projectId: string }
>("projectIssues:createFromSignal");

const beginIssue = makeFunctionReference<
  "mutation",
  { readonly issueId: string },
  "queued" | "working"
>("projectIssues:begin");

const markDispatchFailed = makeFunctionReference<
  "mutation",
  { readonly error: string; readonly issueId: string },
  null
>("projectIssues:markDispatchFailed");

const invalidRequest = (message: string) =>
  Response.json({ error: message }, { status: 400 });

const knownAuthorizationFailure = (message: string): boolean =>
  /authentication required|membership required|project not found|signal not found|not project-scoped/iu.test(
    message
  );

const errorMessage = (error: unknown): string =>
  error instanceof Error ? error.message : String(error);

const decodeRequest = async (input: unknown): Promise<ProjectIssueRequest> => {
  try {
    return await Effect.runPromise(decodeProjectIssueRequest(input));
  } catch (error) {
    if (
      error instanceof ProjectIssueRequestError ||
      error instanceof ProjectIssueValidationError
    ) {
      throw invalidRequest(
        error instanceof Error ? error.message : "Invalid project request"
      );
    }
    throw error;
  }
};

const createIssueForRequest = async (
  client: ConvexHttpClient,
  request: Awaited<ReturnType<typeof decodeRequest>>
): Promise<{ readonly issueId: string; readonly projectId: string }> => {
  if (request.kind === "signal") {
    return client.mutation(createIssueFromSignal, {
      signalId: request.signalId,
    });
  }
  const issueId = await client.mutation(createIssue, {
    body: request.body,
    projectId: request.projectId,
    title: request.title,
  });
  return { issueId, projectId: request.projectId };
};

export const projectRequestRoute = async (c: Context): Promise<Response> => {
  const accessToken = extractBearerToken(c.req.raw);
  if (!accessToken) {
    return c.json({ error: "Unauthorized" }, 401);
  }

  let input: unknown;
  try {
    input = await c.req.json();
  } catch {
    return invalidRequest("Request body must be valid JSON");
  }

  let request: Awaited<ReturnType<typeof decodeRequest>>;
  try {
    request = await decodeRequest(input);
  } catch (error) {
    if (error instanceof Response) {
      return error;
    }
    return c.json({ error: "Invalid project request" }, 400);
  }

  const client = createAuthenticatedClient(accessToken);
  let issue:
    | { readonly issueId: string; readonly projectId: string }
    | undefined;
  try {
    issue = await createIssueForRequest(client, request);
    const status = await client.mutation(beginIssue, {
      issueId: issue.issueId,
    });
    const dispatchInput = Schema.decodeUnknownSync(ProjectIssueDispatchInput)({
      issueId: issue.issueId,
      kind: "project.issue.started",
      projectId: issue.projectId,
    });
    const receipt = await dispatch({
      agent: "project-manager",
      id: issue.issueId,
      input: dispatchInput,
    });
    const result = Schema.decodeUnknownSync(ProjectIssueRequestResult)({
      acceptedAt: receipt.acceptedAt,
      dispatchId: receipt.dispatchId,
      issueId: issue.issueId,
      projectId: issue.projectId,
      status,
    });
    return c.json(result, 202);
  } catch (error) {
    const message = errorMessage(error);
    if (issue) {
      try {
        await client.mutation(markDispatchFailed, {
          error: message,
          issueId: issue.issueId,
        });
      } catch {
        // Preserve the original request failure; the issue remains inspectable.
      }
    }
    return c.json(
      {
        error: knownAuthorizationFailure(message)
          ? "Project request is not authorized"
          : "Project request could not be dispatched",
      },
      knownAuthorizationFailure(message) ? 403 : 502
    );
  }
};
