/**
 * Monitor authorization policy.
 *
 * Single source of truth for "can this user touch this monitor?". Used by
 * MonitorService mutations and any route that takes a monitor id from
 * untrusted input.
 *
 * Rules (mirrors existing scattered patterns in email.service.ts and
 * analytics.ts — unified here, not invented):
 *
 *   | Role             | Read         | Write        | Delete       |
 *   | ADMIN            | any          | any          | any          |
 *   | ADMIN_READ_ONLY  | any          | denied       | denied       |
 *   | EDITOR           | own only     | own only     | own only     |
 *   | VIEWER           | own only     | denied       | denied       |
 *
 * Soft-deleted monitors throw NotFoundError (404) — we don't leak the
 * existence of someone else's IDs to non-owners. Non-existent monitors
 * throw NotFoundError too.
 */
import { prisma } from '@/lib/prisma';
import { AuthContext, AuthorizationError, NotFoundError } from './types';

interface MonitorOwnership {
    userId: number;
    deletedAt: Date | null;
}

async function loadOwnership(id: number): Promise<MonitorOwnership> {
    const monitor = await prisma.monitor.findUnique({
        where: { id },
        select: { userId: true, deletedAt: true },
    });
    if (!monitor) throw new NotFoundError(`Monitor ${id} not found`);
    if (monitor.deletedAt) throw new NotFoundError(`Monitor ${id} not found`);
    return monitor;
}

async function loadOwnershipMany(ids: number[]): Promise<Map<number, MonitorOwnership>> {
    const rows = await prisma.monitor.findMany({
        where: { id: { in: ids } },
        select: { id: true, userId: true, deletedAt: true },
    });
    const map = new Map<number, MonitorOwnership>();
    for (const row of rows) {
        if (!row.deletedAt) {
            map.set(row.id, { userId: row.userId, deletedAt: row.deletedAt });
        }
    }
    return map;
}

export const MonitorAuthorization = {
    canReadAny(ctx: AuthContext): boolean {
        return ctx.role === 'ADMIN' || ctx.role === 'ADMIN_READ_ONLY';
    },

    canWriteAny(ctx: AuthContext): boolean {
        return ctx.role === 'ADMIN';
    },

    canDeleteAny(ctx: AuthContext): boolean {
        return ctx.role === 'ADMIN';
    },

    async assertCanRead(ctx: AuthContext, monitorId: number): Promise<void> {
        if (this.canReadAny(ctx)) {
            await loadOwnership(monitorId); // still verify existence
            return;
        }
        const ownership = await loadOwnership(monitorId);
        if (ownership.userId !== ctx.userId) {
            throw new AuthorizationError('You do not have permission to view this monitor');
        }
    },

    async assertCanWrite(ctx: AuthContext, monitorId: number): Promise<void> {
        if (ctx.role === 'VIEWER' || ctx.role === 'ADMIN_READ_ONLY') {
            await loadOwnership(monitorId); // 404 before 403
            throw new AuthorizationError(`Role ${ctx.role} cannot modify monitors`);
        }
        if (this.canWriteAny(ctx)) {
            await loadOwnership(monitorId);
            return;
        }
        // EDITOR — must own
        const ownership = await loadOwnership(monitorId);
        if (ownership.userId !== ctx.userId) {
            throw new AuthorizationError('You do not have permission to modify this monitor');
        }
    },

    async assertCanDelete(ctx: AuthContext, monitorId: number): Promise<void> {
        // Same policy as write: ADMIN any, EDITOR own, others denied.
        await this.assertCanWrite(ctx, monitorId);
    },

    /**
     * Atomic: throws if *any* monitor in the list is missing or not writable
     * by the caller. Use for bulk operations where partial success would be
     * confusing.
     */
    async assertCanWriteMany(ctx: AuthContext, monitorIds: number[]): Promise<void> {
        if (monitorIds.length === 0) return;
        if (ctx.role === 'VIEWER' || ctx.role === 'ADMIN_READ_ONLY') {
            throw new AuthorizationError(`Role ${ctx.role} cannot modify monitors`);
        }
        const ownership = await loadOwnershipMany(monitorIds);
        for (const id of monitorIds) {
            const row = ownership.get(id);
            if (!row) throw new NotFoundError(`Monitor ${id} not found`);
            if (!this.canWriteAny(ctx) && row.userId !== ctx.userId) {
                throw new AuthorizationError(`You do not have permission to modify monitor ${id}`);
            }
        }
    },

    /**
     * Soft variant: returns only the IDs the caller may write to, silently
     * dropping the rest. Use for best-effort batch operations (e.g. sparkline
     * fetches) where partial results are acceptable.
     */
    async filterWritable(ctx: AuthContext, monitorIds: number[]): Promise<number[]> {
        if (monitorIds.length === 0) return [];
        if (ctx.role === 'VIEWER' || ctx.role === 'ADMIN_READ_ONLY') return [];
        if (this.canWriteAny(ctx)) {
            // Drop soft-deleted but keep everything else
            const ownership = await loadOwnershipMany(monitorIds);
            return monitorIds.filter(id => ownership.has(id));
        }
        const ownership = await loadOwnershipMany(monitorIds);
        return monitorIds.filter(id => {
            const row = ownership.get(id);
            return row !== undefined && row.userId === ctx.userId;
        });
    },
};
