import type { Prisma } from "@prisma/client";
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "../../../lib/server/db";
import { getSessionPepper, isDatabaseConfigured } from "../../../lib/server/env";
import {
  hashSessionToken,
  hashRuleClaimToken,
  newSessionToken,
} from "../../../lib/server/hash";
import { sendRuleStakeholderInviteEmail } from "../../../lib/server/mail";
import { rateLimitKey } from "../../../lib/server/rateLimit";
import {
  dbUnavailable,
  errorJson,
  rateLimited,
  serverMisconfigured,
} from "../../../lib/server/responses";
import { logRouteError } from "../../../lib/server/requestId";
import { stakeholderInviteVerifyUrl } from "../../../lib/server/ruleStakeholderInviteOps";
import { STAKEHOLDER_INVITE_TTL_MS } from "../../../lib/server/ruleStakeholders";
import { getSessionUser } from "../../../lib/server/session";
import { issueGuestRuleClaimCookie } from "../../../lib/server/guestRuleClaim";
import { apiRoute } from "../../../lib/server/apiRoute";
import { getPublicOrigin } from "../../../lib/server/publicOrigin";
import {
  publishRuleBodySchema,
  uniqueStakeholderEmailsForPublish,
} from "../../../lib/server/validation/createFlowSchemas";
import { readLimitedJson } from "../../../lib/server/validation/requestBody";
import { jsonFromZodError } from "../../../lib/server/validation/zodHttp";

/** One anonymous publish per IP per minute (in-memory limiter). */
const ANONYMOUS_PUBLISH_IP_MIN_INTERVAL_MS = 60_000;

function clientIp(request: NextRequest): string {
  return (
    request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ??
    request.headers.get("x-real-ip") ??
    "unknown"
  );
}

export const GET = apiRoute("rules.list", async (request: NextRequest) => {
  if (!isDatabaseConfigured()) {
    return dbUnavailable();
  }

  const { searchParams } = new URL(request.url);
  const take = Math.min(Number(searchParams.get("limit") ?? "50") || 50, 100);

  /**
   * Public catalog: owned rules only (last touched first). Guest / orphan rows
   * (`userId` null) stay reachable at `/rules/[id]` and `GET /api/rules/[id]`.
   */
  const rules = await prisma.publishedRule.findMany({
    where: { userId: { not: null } },
    orderBy: [{ updatedAt: "desc" }, { id: "asc" }],
    take,
    select: {
      id: true,
      title: true,
      summary: true,
      createdAt: true,
      updatedAt: true,
    },
  });

  return NextResponse.json({ rules });
});

export const POST = apiRoute(
  "rules.publish",
  async (request: NextRequest, _ctx, { requestId }) => {
    if (!isDatabaseConfigured()) {
      return dbUnavailable();
    }

    const user = await getSessionUser();

    const parsedBody = await readLimitedJson(request);
    if (parsedBody.ok === false) {
      return parsedBody.response;
    }

    const validated = publishRuleBodySchema.safeParse(parsedBody.value);
    if (!validated.success) {
      return jsonFromZodError(validated.error);
    }

    const { title, summary, document, stakeholderEmails } = validated.data;

    if (!user) {
      const ip = clientIp(request);
      const rl = rateLimitKey(
        `publish-anonymous-ip:${ip}`,
        ANONYMOUS_PUBLISH_IP_MIN_INTERVAL_MS,
      );
      if (rl.ok === false) {
        return rateLimited(rl.retryAfterMs);
      }

      let pepper: string;
      try {
        pepper = getSessionPepper();
      } catch (err) {
        logRouteError("rules.publish", requestId, err, {
          phase: "getSessionPepper",
        });
        return serverMisconfigured();
      }

      const claimToken = newSessionToken();
      const claimTokenHash = hashRuleClaimToken(claimToken, pepper);

      const rule = await prisma.publishedRule.create({
        data: {
          userId: null,
          title,
          summary,
          document: document as Prisma.InputJsonValue,
          claimTokenHash,
        },
      });

      try {
        await issueGuestRuleClaimCookie(claimToken);
      } catch (err) {
        logRouteError("rules.publish", requestId, err, {
          phase: "issueGuestRuleClaimCookie",
        });
      }

      return NextResponse.json({
        rule: {
          id: rule.id,
          title: rule.title,
          summary: rule.summary,
          createdAt: rule.createdAt,
        },
      });
    }

    const inviteEmails = uniqueStakeholderEmailsForPublish(
      stakeholderEmails,
      user.email,
    );

    if (inviteEmails.length > 0) {
      const ip = clientIp(request);
      const rl = rateLimitKey(`publish-stakeholders-ip:${ip}`, 60_000);
      if (rl.ok === false) {
        return rateLimited(rl.retryAfterMs);
      }
    }

    if (inviteEmails.length === 0) {
      const rule = await prisma.publishedRule.create({
        data: {
          userId: user.id,
          title,
          summary,
          document: document as Prisma.InputJsonValue,
        },
      });

      return NextResponse.json({
        rule: {
          id: rule.id,
          title: rule.title,
          summary: rule.summary,
          createdAt: rule.createdAt,
        },
      });
    }

    let pepper: string;
    try {
      pepper = getSessionPepper();
    } catch (err) {
      logRouteError("rules.publish", requestId, err, {
        phase: "getSessionPepper",
      });
      return serverMisconfigured();
    }

    const expiresAt = new Date(Date.now() + STAKEHOLDER_INVITE_TTL_MS);
    const { rule, invites } = await prisma.$transaction(async (tx) => {
      const created = await tx.publishedRule.create({
        data: {
          userId: user.id,
          title,
          summary,
          document: document as Prisma.InputJsonValue,
        },
      });
      const toSend: { email: string; token: string }[] = [];
      for (const email of inviteEmails) {
        const token = newSessionToken();
        const tokenHash = hashSessionToken(token, pepper);
        await tx.ruleStakeholder.create({
          data: {
            ruleId: created.id,
            email,
            invitedByUserId: user.id,
            inviteTokenHash: tokenHash,
            inviteExpiresAt: expiresAt,
          },
        });
        toSend.push({ email, token });
      }
      return { rule: created, invites: toSend };
    });

    const origin = getPublicOrigin(request);
    try {
      for (const inv of invites) {
        const verifyUrl = stakeholderInviteVerifyUrl(origin, inv.token);
        await sendRuleStakeholderInviteEmail(inv.email, verifyUrl, title);
      }
    } catch (err) {
      logRouteError("rules.publish", requestId, err, {
        phase: "sendRuleStakeholderInviteEmail",
      });
      try {
        await prisma.publishedRule.delete({ where: { id: rule.id } });
      } catch (delErr) {
        logRouteError("rules.publish", requestId, delErr, {
          phase: "rollbackPublishAfterMailFailure",
        });
      }
      return errorJson(
        "mail_failed",
        "Could not send stakeholder invites",
        502,
      );
    }

    return NextResponse.json({
      rule: {
        id: rule.id,
        title: rule.title,
        summary: rule.summary,
        createdAt: rule.createdAt,
      },
    });
  },
);
