/**
 * HTTP guards for org admin APIs (ADR-0021).
 *
 * Platform admin is a separate control plane — not modeled here.
 */
import type { Organization, OrganizationMemberRole, PrismaClient, User } from "@prisma/client";
import type { FastifyReply, FastifyRequest } from "fastify";
import {
  SESSION_COOKIE_NAME,
  verifySession,
  type SessionPayload,
} from "./session.js";
import { createPermissionAuthorizer, type AuthorizationAction } from "../../permissions/authorizer.js";

export const ORG_ADMIN_ROLES: readonly OrganizationMemberRole[] = ["OWNER", "ADMIN"];
const ANY_ORG_ROLE: readonly OrganizationMemberRole[] = ["OWNER", "ADMIN", "MEMBER"];

export interface AuthContext {
  readonly session: SessionPayload;
  readonly user: User;
  readonly feishuOpenId: string;
  readonly authenticationOrganizationId?: string | undefined;
}

export interface OrgAuthContext extends AuthContext {
  readonly organization: Organization;
  readonly membershipRole: OrganizationMemberRole;
}

export interface GuardDeps {
  readonly prisma: PrismaClient;
  readonly sessionSecret: string;
}

export class HttpError extends Error {
  readonly statusCode: number;
  readonly code: string;

  constructor(statusCode: number, code: string, message: string) {
    super(message);
    this.name = "HttpError";
    this.statusCode = statusCode;
    this.code = code;
  }
}

export async function requireSession(
  request: FastifyRequest,
  reply: FastifyReply,
  deps: GuardDeps,
): Promise<AuthContext | null> {
  const raw = request.cookies[SESSION_COOKIE_NAME];
  if (raw === undefined || raw === "") {
    await sendError(reply, 401, "unauthenticated", "login required");
    return null;
  }
  const session = verifySession(raw, deps.sessionSecret);
  if (session === null) {
    await sendError(reply, 401, "unauthenticated", "session invalid or expired");
    return null;
  }
  if ("feishuIdentityId" in session) {
    const identity = await deps.prisma.feishuUserIdentity.findUnique({
      where: { id: session.feishuIdentityId },
      include: {
        user: true,
        connection: {
          select: {
            id: true,
            status: true,
            activeSecretVersion: { select: { connectionId: true, retiredAt: true } },
            organization: { select: { id: true, status: true } },
          },
        },
      },
    });
    if (identity === null || identity.userId !== session.userId ||
        identity.connectionId !== session.feishuConnectionId ||
        identity.connection.organization.id !== session.feishuOrganizationId ||
        identity.connection.status !== "ACTIVE" ||
        identity.connection.activeSecretVersion === null ||
        identity.connection.activeSecretVersion.connectionId !== identity.connection.id ||
        identity.connection.activeSecretVersion.retiredAt !== null ||
        identity.connection.organization.status !== "ACTIVE") {
      await sendError(reply, 401, "unauthenticated", "session identity not found or inactive");
      return null;
    }
    return {
      session,
      user: identity.user,
      feishuOpenId: identity.openId,
      authenticationOrganizationId: identity.connection.organization.id,
    };
  }
  const user = await deps.prisma.user.findUnique({ where: { id: session.userId } });
  if (user === null || user.feishuOpenId !== session.feishuOpenId) {
    await sendError(reply, 401, "unauthenticated", "session user not found");
    return null;
  }
  return { session, user, feishuOpenId: user.feishuOpenId };
}

export async function requireOrgRole(
  request: FastifyRequest,
  reply: FastifyReply,
  deps: GuardDeps,
  options: {
    readonly orgSlug: string;
    readonly roles?: readonly OrganizationMemberRole[];
  },
): Promise<OrgAuthContext | null> {
  const auth = await requireSession(request, reply, deps);
  if (auth === null) return null;

  const allowed = options.roles ?? ORG_ADMIN_ROLES;
  const organization = await deps.prisma.organization.findUnique({
    where: { slug: options.orgSlug },
  });
  if (organization === null) {
    await sendError(reply, 404, "org_not_found", `organization not found: ${options.orgSlug}`);
    return null;
  }
  if (organization.status !== "ACTIVE") {
    await sendError(reply, 403, "org_not_active", `organization is ${organization.status}`);
    return null;
  }
  if (auth.authenticationOrganizationId !== undefined &&
      auth.authenticationOrganizationId !== organization.id) {
    await sendError(reply, 403, "forbidden", "session is scoped to a different organization");
    return null;
  }

  const membership = await deps.prisma.organizationMembership.findFirst({
    where: {
      organizationId: organization.id,
      userId: auth.user.id,
      revokedAt: null,
    },
    select: { role: true },
  });
  if (membership === null) {
    await sendError(reply, 403, "forbidden", "not a member of this organization");
    return null;
  }
  if (!allowed.includes(membership.role)) {
    await sendError(reply, 403, "forbidden", `requires role: ${allowed.join("|")}`);
    return null;
  }

  return {
    ...auth,
    organization,
    membershipRole: membership.role,
  };
}

export async function requireOrgProject(
  deps: GuardDeps,
  organizationId: string,
  projectId: string,
): Promise<{ readonly id: string; readonly organizationId: string; readonly name: string }> {
  const project = await deps.prisma.project.findUnique({
    where: { id: projectId },
    select: { id: true, organizationId: true, name: true, archivedAt: true },
  });
  if (project === null || project.organizationId !== organizationId) {
    throw new HttpError(404, "project_not_found", `project not found: ${projectId}`);
  }
  return project;
}

export async function sendError(
  reply: FastifyReply,
  statusCode: number,
  code: string,
  message: string,
): Promise<void> {
  await reply.status(statusCode).send({ error: { code, message } });
}

export interface ProjectAuthContext extends OrgAuthContext {
  readonly projectId: string;
}

/**
 * Resolve an org member (any role) viewing/mutating a project in their org, then
 * enforce project-level permission via the PermissionGrant authorizer.
 *
 * `allowOrgAdminOversight=true` lets org OWNER/ADMIN through without a project
 * grant — reserved for *read* oversight (listing/viewing). Mutations that the
 * spec pins to project `manage` (e.g. `collaborator.manage`) must pass
 * `allowOrgAdminOversight=false`: org role alone is not a project authorization
 * root (spec `Permission.lean` / ADR-0004; the only out-of-role override is
 * platform-admin force-release `RequiresAdmin`, not org admin).
 */
export async function requireProjectPermission(
  request: FastifyRequest,
  reply: FastifyReply,
  deps: GuardDeps,
  options: {
    readonly orgSlug: string;
    readonly projectId: string;
    readonly action: AuthorizationAction;
    readonly allowOrgAdminOversight: boolean;
  },
): Promise<ProjectAuthContext | null> {
  const auth = await requireOrgRole(request, reply, deps, {
    orgSlug: options.orgSlug,
    roles: ANY_ORG_ROLE,
  });
  if (auth === null) return null;
  await requireOrgProject(deps, auth.organization.id, options.projectId);
  if (options.allowOrgAdminOversight && ORG_ADMIN_ROLES.includes(auth.membershipRole)) {
    return { ...auth, projectId: options.projectId };
  }
  const decision = await createPermissionAuthorizer(deps.prisma).can({
    actor: { feishuOpenId: auth.feishuOpenId },
    action: options.action,
    resource: { type: "PROJECT", id: options.projectId },
  });
  if (!decision.allowed) {
    await sendError(
      reply,
      403,
      "forbidden",
      `project ${options.projectId} requires ${decision.requiredRole} (${options.action}): ${decision.reason}`,
    );
    return null;
  }
  return { ...auth, projectId: options.projectId };
}
