import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'node:crypto';

import { isCanonicalIsoTimestamp } from './accountIconProjection.js';

export interface UserSanctions {
    bannedUntil?: string;
    mutedUntil?: string;
    suspendedUntil?: string;
    warningCount?: number;
    flags?: string[];
    notes?: string;
    serverRestrictions?: Record<string, UserServerRestriction>;
    legacyPenalty?: Record<string, unknown>;
}

export interface UserServerRestriction {
    blockedFeatures?: string[];
    until?: string;
    reason?: string;
    notes?: string;
}

export interface GatewayUserInfo {
    id: string;
    username: string;
    displayName: string;
    identityRevision?: string;
    roles: string[];
    picture?: string;
    imageServer?: number;
    iconUpdatedAt?: string;
    profileIconResetAt?: string;
    canUseGeneralPicture?: boolean;
    createdAt?: string;
    legacyMemberNo?: number;
    icons?: GatewayUserIconInfo[];
}

export interface GatewayUserIconInfo {
    id: string;
    picture: string;
    imageServer: number;
    createdAt: string;
}

export interface GameSessionTokenPayload {
    version: 1;
    profile: string;
    issuedAt: string;
    expiresAt: string;
    sessionId: string;
    user: GatewayUserInfo;
    sanctions: UserSanctions;
    identity?: {
        kakaoVerified: boolean;
        canCreateGeneral: boolean;
        requiresKakaoVerification: boolean;
        graceEndsAt: string | null;
        specialAccess?: {
            kind: 'OPERATOR' | 'TESTER' | 'RECOVERY' | 'OTHER';
            expiresAt: string | null;
        };
    };
}

const toBase64Url = (data: Buffer): string => data.toString('base64url');
const fromBase64Url = (value: string): Buffer => Buffer.from(value, 'base64url');

const buildKey = (secret: string): Buffer => createHash('sha256').update(secret).digest();

export const encryptGameSessionToken = (payload: GameSessionTokenPayload, secret: string): string => {
    const iv = randomBytes(12);
    const key = buildKey(secret);
    const cipher = createCipheriv('aes-256-gcm', key, iv);
    const plaintext = Buffer.from(JSON.stringify(payload), 'utf8');
    const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
    const tag = cipher.getAuthTag();
    return `${toBase64Url(iv)}.${toBase64Url(ciphertext)}.${toBase64Url(tag)}`;
};

export const parseGameSessionTokenPayload = (value: unknown): GameSessionTokenPayload | null => {
    if (!value || typeof value !== 'object') {
        return null;
    }
    const payload = value as Partial<GameSessionTokenPayload>;
    if (payload.version !== 1) {
        return null;
    }
    if (typeof payload.profile !== 'string') {
        return null;
    }
    if (typeof payload.issuedAt !== 'string' || typeof payload.expiresAt !== 'string') {
        return null;
    }
    if (typeof payload.sessionId !== 'string') {
        return null;
    }
    if (!payload.user || typeof payload.user !== 'object') {
        return null;
    }
    const user = payload.user as Partial<GatewayUserInfo>;
    if (
        typeof user.id !== 'string' ||
        typeof user.username !== 'string' ||
        typeof user.displayName !== 'string' ||
        (user.identityRevision !== undefined &&
            (typeof user.identityRevision !== 'string' || !isCanonicalIsoTimestamp(user.identityRevision))) ||
        !Array.isArray(user.roles) ||
        (user.picture !== undefined && typeof user.picture !== 'string') ||
        (user.imageServer !== undefined && (!Number.isSafeInteger(user.imageServer) || user.imageServer < 0)) ||
        (user.iconUpdatedAt !== undefined &&
            (typeof user.iconUpdatedAt !== 'string' || !isCanonicalIsoTimestamp(user.iconUpdatedAt))) ||
        (user.profileIconResetAt !== undefined &&
            (typeof user.profileIconResetAt !== 'string' || !isCanonicalIsoTimestamp(user.profileIconResetAt))) ||
        (user.canUseGeneralPicture !== undefined && typeof user.canUseGeneralPicture !== 'boolean') ||
        (user.icons !== undefined &&
            (!Array.isArray(user.icons) ||
                user.icons.length > 5 ||
                user.icons.some(
                    (icon) =>
                        !icon ||
                        typeof icon !== 'object' ||
                        typeof icon.id !== 'string' ||
                        typeof icon.picture !== 'string' ||
                        !Number.isSafeInteger(icon.imageServer) ||
                        icon.imageServer < 0 ||
                        typeof icon.createdAt !== 'string' ||
                        !isCanonicalIsoTimestamp(icon.createdAt)
                ))) ||
        (user.legacyMemberNo !== undefined && (!Number.isSafeInteger(user.legacyMemberNo) || user.legacyMemberNo <= 0))
    ) {
        return null;
    }
    if (!payload.sanctions || typeof payload.sanctions !== 'object') {
        return null;
    }
    if (payload.identity !== undefined) {
        if (!payload.identity || typeof payload.identity !== 'object') {
            return null;
        }
        const identity = payload.identity as Partial<NonNullable<GameSessionTokenPayload['identity']>>;
        if (
            typeof identity.kakaoVerified !== 'boolean' ||
            typeof identity.canCreateGeneral !== 'boolean' ||
            typeof identity.requiresKakaoVerification !== 'boolean' ||
            (identity.graceEndsAt !== null && typeof identity.graceEndsAt !== 'string') ||
            (identity.specialAccess !== undefined &&
                (!identity.specialAccess ||
                    typeof identity.specialAccess !== 'object' ||
                    !['OPERATOR', 'TESTER', 'RECOVERY', 'OTHER'].includes(identity.specialAccess.kind) ||
                    (identity.specialAccess.expiresAt !== null &&
                        typeof identity.specialAccess.expiresAt !== 'string')))
        ) {
            return null;
        }
    }
    return payload as GameSessionTokenPayload;
};

export const decryptGameSessionToken = (token: string, secret: string): GameSessionTokenPayload | null => {
    const parts = token.split('.');
    if (parts.length !== 3) {
        return null;
    }
    try {
        const [ivPart, cipherPart, tagPart] = parts;
        const iv = fromBase64Url(ivPart);
        const ciphertext = fromBase64Url(cipherPart);
        const tag = fromBase64Url(tagPart);
        const key = buildKey(secret);
        const decipher = createDecipheriv('aes-256-gcm', key, iv);
        decipher.setAuthTag(tag);
        const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf8');
        return parseGameSessionTokenPayload(JSON.parse(plaintext));
    } catch {
        return null;
    }
};
