import { randomUUID } from 'node:crypto';
import { PersistentStore } from '../state/persistent-store.js';
const MAX_AUDIT_RECORDS = 500;
function defaultPolicy(surface) {
    return {
        surface,
        enabled: true,
        requireMention: false,
        allowDirectMessages: true,
        allowGroupMessages: true,
        allowThreadMessages: true,
        dmPolicy: 'inherit',
        groupPolicy: 'inherit',
        allowTextCommandsWithoutMention: false,
        allowlistUserIds: [],
        allowlistChannelIds: [],
        allowlistGroupIds: [],
        allowedCommands: [],
        groupPolicies: [],
        updatedAt: Date.now(),
        metadata: {},
    };
}
function normalizeGroupPolicy(policy) {
    return {
        ...policy,
        metadata: policy.metadata ?? {},
    };
}
function normalizeConversationKind(input) {
    if (input.conversationKind)
        return input.conversationKind;
    if (input.threadId)
        return 'thread';
    if (input.groupId)
        return 'group';
    if (input.channelId)
        return 'channel';
    return 'service';
}
function firstCommand(text, explicit) {
    if (explicit && explicit.trim().length > 0)
        return explicit.trim().toLowerCase();
    if (!text)
        return '';
    return text.trim().split(/\s+/, 1)[0]?.toLowerCase() ?? '';
}
function commandAllowed(command, allowedCommands) {
    if (allowedCommands.length === 0 || !command)
        return true;
    return allowedCommands.map((entry) => entry.toLowerCase()).includes(command);
}
const CONVERSATION_POLICIES = new Set(['allow', 'deny', 'inherit']);
const CONVERSATION_KINDS = new Set(['direct', 'group', 'channel', 'thread', 'service']);
function isRecord(value) {
    return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function isFiniteNumber(value) {
    return typeof value === 'number' && Number.isFinite(value);
}
function isStringArray(value) {
    return Array.isArray(value) && value.every((entry) => typeof entry === 'string');
}
function throwInvalidChannelPolicySnapshot() {
    throw new Error('Channel policy store snapshot is invalid.');
}
function validateOptionalString(value) {
    if (value !== undefined && typeof value !== 'string')
        throwInvalidChannelPolicySnapshot();
}
function validateOptionalBoolean(value) {
    if (value !== undefined && typeof value !== 'boolean')
        throwInvalidChannelPolicySnapshot();
}
function validateOptionalStringArray(value) {
    if (value !== undefined && !isStringArray(value))
        throwInvalidChannelPolicySnapshot();
}
function validateChannelGroupPolicyRecord(value) {
    if (!isRecord(value) || typeof value['id'] !== 'string')
        throwInvalidChannelPolicySnapshot();
    validateOptionalString(value['label']);
    validateOptionalString(value['groupId']);
    validateOptionalString(value['channelId']);
    validateOptionalString(value['workspaceId']);
    validateOptionalBoolean(value['requireMention']);
    validateOptionalBoolean(value['allowGroupMessages']);
    validateOptionalBoolean(value['allowThreadMessages']);
    validateOptionalBoolean(value['allowTextCommandsWithoutMention']);
    validateOptionalStringArray(value['allowlistUserIds']);
    validateOptionalStringArray(value['allowlistChannelIds']);
    validateOptionalStringArray(value['allowlistGroupIds']);
    validateOptionalStringArray(value['allowedCommands']);
    if (value['metadata'] !== undefined && !isRecord(value['metadata']))
        throwInvalidChannelPolicySnapshot();
}
function validateChannelPolicyRecord(value) {
    if (!isRecord(value) || typeof value['surface'] !== 'string')
        throwInvalidChannelPolicySnapshot();
    validateOptionalBoolean(value['enabled']);
    validateOptionalBoolean(value['requireMention']);
    validateOptionalBoolean(value['allowDirectMessages']);
    validateOptionalBoolean(value['allowGroupMessages']);
    validateOptionalBoolean(value['allowThreadMessages']);
    validateOptionalBoolean(value['allowTextCommandsWithoutMention']);
    const dmPolicy = value['dmPolicy'];
    if (dmPolicy !== undefined && (typeof dmPolicy !== 'string' || !CONVERSATION_POLICIES.has(dmPolicy))) {
        throwInvalidChannelPolicySnapshot();
    }
    const groupPolicy = value['groupPolicy'];
    if (groupPolicy !== undefined && (typeof groupPolicy !== 'string' || !CONVERSATION_POLICIES.has(groupPolicy))) {
        throwInvalidChannelPolicySnapshot();
    }
    validateOptionalStringArray(value['allowlistUserIds']);
    validateOptionalStringArray(value['allowlistChannelIds']);
    validateOptionalStringArray(value['allowlistGroupIds']);
    validateOptionalStringArray(value['allowedCommands']);
    if (value['groupPolicies'] !== undefined) {
        if (!Array.isArray(value['groupPolicies']))
            throwInvalidChannelPolicySnapshot();
        for (const groupPolicy of value['groupPolicies'])
            validateChannelGroupPolicyRecord(groupPolicy);
    }
    if (value['updatedAt'] !== undefined && !isFiniteNumber(value['updatedAt']))
        throwInvalidChannelPolicySnapshot();
    if (value['metadata'] !== undefined && !isRecord(value['metadata']))
        throwInvalidChannelPolicySnapshot();
}
function validateChannelPolicyAuditRecord(value) {
    if (!isRecord(value)
        || typeof value['id'] !== 'string'
        || typeof value['surface'] !== 'string'
        || !isFiniteNumber(value['createdAt'])
        || typeof value['allowed'] !== 'boolean'
        || typeof value['reason'] !== 'string'
        || !isRecord(value['metadata'])) {
        throwInvalidChannelPolicySnapshot();
    }
    validateOptionalString(value['userId']);
    validateOptionalString(value['channelId']);
    validateOptionalString(value['groupId']);
    validateOptionalString(value['threadId']);
    validateOptionalString(value['matchedGroupPolicyId']);
    validateOptionalString(value['text']);
    const conversationKind = value['conversationKind'];
    if (conversationKind !== undefined
        && (typeof conversationKind !== 'string' || !CONVERSATION_KINDS.has(conversationKind))) {
        throwInvalidChannelPolicySnapshot();
    }
}
function validateChannelPolicySnapshot(snapshot) {
    if (!snapshot)
        return null;
    if (!isRecord(snapshot) || !Array.isArray(snapshot.policies) || !Array.isArray(snapshot.audit))
        throwInvalidChannelPolicySnapshot();
    for (const policy of snapshot.policies)
        validateChannelPolicyRecord(policy);
    for (const audit of snapshot.audit)
        validateChannelPolicyAuditRecord(audit);
    return snapshot;
}
export class ChannelPolicyManager {
    store;
    policies = new Map();
    audit = [];
    loaded = false;
    constructor(options) {
        if (options.store) {
            this.store = options.store;
            return;
        }
        if (!options.storePath) {
            throw new Error('ChannelPolicyManager requires an explicit store or storePath');
        }
        this.store = new PersistentStore(options.storePath);
    }
    async start() {
        if (this.loaded)
            return;
        const snapshot = validateChannelPolicySnapshot(await this.store.load());
        this.policies.clear();
        this.audit.length = 0;
        for (const policy of snapshot?.policies ?? []) {
            this.policies.set(policy.surface, {
                ...defaultPolicy(policy.surface),
                ...policy,
                allowDirectMessages: policy.allowDirectMessages ?? true,
                allowGroupMessages: policy.allowGroupMessages ?? true,
                allowThreadMessages: policy.allowThreadMessages ?? true,
                dmPolicy: policy.dmPolicy ?? 'inherit',
                groupPolicy: policy.groupPolicy ?? 'inherit',
                allowTextCommandsWithoutMention: policy.allowTextCommandsWithoutMention ?? false,
                allowlistGroupIds: policy.allowlistGroupIds ?? [],
                groupPolicies: (policy.groupPolicies ?? []).map(normalizeGroupPolicy),
                metadata: policy.metadata ?? {},
            });
        }
        this.audit.push(...(snapshot?.audit ?? []));
        this.loaded = true;
    }
    listPolicies() {
        return [...this.policies.values()].sort((a, b) => a.surface.localeCompare(b.surface));
    }
    listAudit(limit = 100) {
        return this.audit.slice(0, Math.max(1, limit));
    }
    getPolicy(surface) {
        return this.policies.get(surface) ?? defaultPolicy(surface);
    }
    async upsertPolicy(surface, patch) {
        await this.start();
        const existing = this.getPolicy(surface);
        const next = {
            ...existing,
            ...patch,
            surface,
            updatedAt: Date.now(),
            allowDirectMessages: patch.allowDirectMessages ?? existing.allowDirectMessages,
            allowGroupMessages: patch.allowGroupMessages ?? existing.allowGroupMessages,
            allowThreadMessages: patch.allowThreadMessages ?? existing.allowThreadMessages,
            dmPolicy: patch.dmPolicy ?? existing.dmPolicy,
            groupPolicy: patch.groupPolicy ?? existing.groupPolicy,
            allowTextCommandsWithoutMention: patch.allowTextCommandsWithoutMention ?? existing.allowTextCommandsWithoutMention,
            allowlistUserIds: patch.allowlistUserIds ?? existing.allowlistUserIds,
            allowlistChannelIds: patch.allowlistChannelIds ?? existing.allowlistChannelIds,
            allowlistGroupIds: patch.allowlistGroupIds ?? existing.allowlistGroupIds,
            allowedCommands: patch.allowedCommands ?? existing.allowedCommands,
            groupPolicies: (patch.groupPolicies ?? existing.groupPolicies).map(normalizeGroupPolicy),
            metadata: patch.metadata ?? existing.metadata,
        };
        this.policies.set(surface, next);
        await this.persist();
        return next;
    }
    async evaluateIngress(input) {
        await this.start();
        const policy = this.getPolicy(input.surface);
        const conversationKind = normalizeConversationKind(input);
        const matchedGroupPolicy = policy.groupPolicies.find((entry) => ((entry.groupId && input.groupId && entry.groupId === input.groupId)
            || (entry.channelId && input.channelId && entry.channelId === input.channelId)
            || (entry.workspaceId && input.workspaceId && entry.workspaceId === input.workspaceId)));
        const requireMention = matchedGroupPolicy?.requireMention ?? policy.requireMention;
        const allowTextCommandsWithoutMention = matchedGroupPolicy?.allowTextCommandsWithoutMention
            ?? policy.allowTextCommandsWithoutMention;
        const allowlistUserIds = matchedGroupPolicy?.allowlistUserIds ?? policy.allowlistUserIds;
        const allowlistChannelIds = matchedGroupPolicy?.allowlistChannelIds ?? policy.allowlistChannelIds;
        const allowlistGroupIds = matchedGroupPolicy?.allowlistGroupIds ?? policy.allowlistGroupIds;
        const allowedCommands = matchedGroupPolicy?.allowedCommands ?? policy.allowedCommands;
        const allowGroupMessages = matchedGroupPolicy?.allowGroupMessages ?? policy.allowGroupMessages;
        const allowThreadMessages = matchedGroupPolicy?.allowThreadMessages ?? policy.allowThreadMessages;
        const command = firstCommand(input.text, input.controlCommand);
        const isAuthorizedControlCommand = commandAllowed(command, allowedCommands);
        const bypassMention = (conversationKind === 'group' || conversationKind === 'channel' || conversationKind === 'thread')
            && requireMention
            && !input.mentioned
            && !input.hasAnyMention
            && allowTextCommandsWithoutMention
            && Boolean(command)
            && isAuthorizedControlCommand;
        let allowed = true;
        let reason = 'allowed';
        if (!policy.enabled) {
            allowed = false;
            reason = 'surface-disabled';
        }
        else if (conversationKind === 'direct' && (policy.dmPolicy === 'deny' || !policy.allowDirectMessages)) {
            allowed = false;
            reason = 'direct-messages-disabled';
        }
        else if ((conversationKind === 'group' || conversationKind === 'channel') && (policy.groupPolicy === 'deny' || !allowGroupMessages)) {
            allowed = false;
            reason = 'group-messages-disabled';
        }
        else if (conversationKind === 'thread' && !allowThreadMessages) {
            allowed = false;
            reason = 'thread-messages-disabled';
        }
        else if (allowlistGroupIds.length > 0 && input.groupId && !allowlistGroupIds.includes(input.groupId)) {
            allowed = false;
            reason = 'group-not-allowlisted';
        }
        else if (allowlistGroupIds.length > 0 && !input.groupId) {
            allowed = false;
            reason = 'missing-group-identity';
        }
        else if (allowlistUserIds.length > 0 && input.userId && !allowlistUserIds.includes(input.userId)) {
            allowed = false;
            reason = 'user-not-allowlisted';
        }
        else if (allowlistUserIds.length > 0 && !input.userId) {
            allowed = false;
            reason = 'missing-user-identity';
        }
        else if (allowlistChannelIds.length > 0 && input.channelId && !allowlistChannelIds.includes(input.channelId)) {
            allowed = false;
            reason = 'channel-not-allowlisted';
        }
        else if (allowlistChannelIds.length > 0 && !input.channelId) {
            allowed = false;
            reason = 'missing-channel-identity';
        }
        else if (requireMention && !input.mentioned && !bypassMention) {
            allowed = false;
            reason = 'mention-required';
        }
        else if (!commandAllowed(command, allowedCommands)) {
            allowed = false;
            reason = 'command-not-allowed';
        }
        this.audit.unshift({
            id: `policy-audit-${randomUUID().slice(0, 8)}`,
            surface: input.surface,
            createdAt: Date.now(),
            allowed,
            reason,
            ...(input.userId ? { userId: input.userId } : {}),
            ...(input.channelId ? { channelId: input.channelId } : {}),
            ...(input.groupId ? { groupId: input.groupId } : {}),
            ...(input.threadId ? { threadId: input.threadId } : {}),
            conversationKind,
            ...(matchedGroupPolicy?.id ? { matchedGroupPolicyId: matchedGroupPolicy.id } : {}),
            ...(input.text ? { text: input.text.slice(0, 200) } : {}),
            metadata: input.metadata ?? {},
        });
        if (this.audit.length > MAX_AUDIT_RECORDS) {
            this.audit.length = MAX_AUDIT_RECORDS;
        }
        await this.persist();
        return {
            allowed,
            reason,
            policy,
            ...(matchedGroupPolicy ? { matchedGroupPolicy } : {}),
            matchedScope: matchedGroupPolicy ? 'group' : 'surface',
            effectiveRequireMention: requireMention,
            effectiveAllowedCommands: allowedCommands,
        };
    }
    async persist() {
        await this.store.persist({
            policies: this.listPolicies(),
            audit: [...this.audit],
        });
    }
}
