"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
    return function (target, key) { decorator(target, key, paramIndex); }
};
var OAuthService_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.OAuthService = void 0;
const api_locale_1 = require("@hed-hog/api-locale");
const api_prisma_1 = require("@hed-hog/api-prisma");
const common_1 = require("@nestjs/common");
const auth_service_1 = require("../auth/auth.service");
const file_service_1 = require("../file/file.service");
const mail_service_1 = require("../mail/mail.service");
const security_service_1 = require("../security/security.service");
const setting_service_1 = require("../setting/setting.service");
const token_service_1 = require("../token/token.service");
const user_constants_1 = require("../user/constants/user.constants");
const user_service_1 = require("../user/user.service");
const oauth_callback_coordinator_service_1 = require("./oauth-callback-coordinator.service");
const oauth_errors_1 = require("./oauth.errors");
const facebook_provider_1 = require("./providers/facebook.provider");
const github_provider_1 = require("./providers/github.provider");
const google_provider_1 = require("./providers/google.provider");
const microsoft_entra_id_provider_1 = require("./providers/microsoft-entra-id.provider");
const microsoft_provider_1 = require("./providers/microsoft.provider");
let OAuthService = OAuthService_1 = class OAuthService {
    constructor(google, facebook, microsoft, github, microsoftEntraId, callbackCoordinator, auth, file, mail, security, prisma, token, setting, user) {
        this.callbackCoordinator = callbackCoordinator;
        this.auth = auth;
        this.file = file;
        this.mail = mail;
        this.security = security;
        this.prisma = prisma;
        this.token = token;
        this.setting = setting;
        this.user = user;
        this.logger = new common_1.Logger(OAuthService_1.name);
        this.providers = new Map();
        this.providers.set(google.getProviderType(), google);
        this.providers.set(facebook.getProviderType(), facebook);
        this.providers.set(microsoft.getProviderType(), microsoft);
        this.providers.set(github.getProviderType(), github);
        this.providers.set(microsoftEntraId.getProviderType(), microsoftEntraId);
    }
    getAuthUrl(provider, callbackPath) {
        return this.getProvider(provider).getAuthUrl(callbackPath);
    }
    async handleCallback({ res, locale, ipAddress, userAgent, provider, code, type, userId, email, }) {
        var _a;
        try {
            this.logger.log(`OAuth callback received: provider=${provider} type=${type} locale=${locale} ip=${ipAddress || 'n/a'} hasCode=${code ? 'yes' : 'no'} codeLength=${(_a = code === null || code === void 0 ? void 0 : code.length) !== null && _a !== void 0 ? _a : 0} userAgent="${this.summarizeUserAgent(userAgent)}"`);
            if (type === 'disconnect') {
                return this.handleDisconnect(res, locale, ipAddress, userAgent, provider, email);
            }
            if (!code) {
                throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('oauth.callback.codeRequired', locale, 'Authorization code is required to complete OAuth authentication.'));
            }
            const callbackKey = this.buildCallbackKey(provider, type, code);
            return await this.callbackCoordinator.execute(callbackKey, async () => {
                const profile = await this.getProvider(provider).getProfile(code, type);
                const result = await this.finalizeCallback({
                    res,
                    locale,
                    ipAddress,
                    userAgent,
                    provider,
                    type,
                    profile,
                    userId,
                });
                this.logger.log(`OAuth callback completed: provider=${provider} type=${type} profileEmailPresent=${(profile === null || profile === void 0 ? void 0 : profile.email) ? 'yes' : 'no'} userId=${userId !== null && userId !== void 0 ? userId : 'n/a'}`);
                return result;
            });
        }
        catch (error) {
            this.logger.warn(`OAuth callback failed before response mapping: provider=${provider} type=${type} message=${error instanceof Error ? error.message : 'unknown error'}`);
            throw this.mapCallbackError(locale, provider, type, error);
        }
    }
    getProvider(provider) {
        const prov = this.providers.get(String(provider).toUpperCase());
        if (!prov)
            throw new common_1.BadRequestException(`Provider ${provider} não suportado`);
        return prov;
    }
    async getProviderScopes(provider) {
        const settingKey = `${String(provider).toLowerCase().replace(/-/g, '_')}_scopes`;
        const settings = await this.setting.getSettingValues([
            'oauth-mfa',
            'google_scopes',
            'facebook_scopes',
            'github_scopes',
            'microsoft_scopes',
            'microsoft_entra_id_scopes',
        ]);
        return settings[settingKey].join(',') || '';
    }
    buildCallbackKey(provider, type, code) {
        return `${String(provider).toLowerCase()}:${type}:${this.security.hashSha256(code)}`;
    }
    summarizeUserAgent(userAgent) {
        if (!userAgent) {
            return 'n/a';
        }
        return userAgent.length > 120
            ? `${userAgent.slice(0, 117)}...`
            : userAgent;
    }
    async finalizeCallback({ res, locale, ipAddress, userAgent, provider, type, profile, userId, }) {
        switch (type) {
            case 'login':
                return this.handleLogin(res, locale, ipAddress, userAgent, provider, profile);
            case 'register':
                return this.handleRegister(res, locale, ipAddress, userAgent, provider, profile);
            case 'connect':
                return this.handleConnect(res, locale, ipAddress, userAgent, provider, profile, userId);
            default:
                throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('oauth.callback.invalidType', locale, 'Unsupported OAuth callback type.'));
        }
    }
    resolveProfileScopes(provider, profile) {
        if (typeof (profile === null || profile === void 0 ? void 0 : profile.oauth_scopes) === 'string') {
            return profile.oauth_scopes;
        }
        return this.getProviderScopes(provider);
    }
    mapCallbackError(locale, provider, type, error) {
        var _a, _b, _c;
        if (error instanceof common_1.BadRequestException ||
            error instanceof common_1.ConflictException ||
            error instanceof common_1.NotFoundException ||
            error instanceof common_1.ServiceUnavailableException) {
            return error;
        }
        if (error instanceof oauth_errors_1.OAuthCallbackAlreadyProcessedError) {
            return new common_1.BadRequestException((0, api_locale_1.getLocaleText)('oauth.callback.alreadyProcessed', locale, 'This OAuth callback has already been processed. Start the sign-in flow again.'));
        }
        if (error instanceof oauth_errors_1.OAuthCallbackInProgressError) {
            return new common_1.ConflictException((0, api_locale_1.getLocaleText)('oauth.callback.inProgress', locale, 'This OAuth callback is already being processed. Wait a moment and try again.'));
        }
        if (error instanceof oauth_errors_1.OAuthProviderException) {
            if (error.reason === 'callback_consumed' || error.reason === 'invalid_callback') {
                return new common_1.BadRequestException((0, api_locale_1.getLocaleText)('oauth.callback.invalid', locale, error.message));
            }
            this.logger.warn(`OAuth callback upstream failure: provider=${provider} type=${type} status=${(_a = error.status) !== null && _a !== void 0 ? _a : 'unknown'} reason=${error.reason} providerError=${(_b = error.providerError) !== null && _b !== void 0 ? _b : 'unknown'} providerCode=${(_c = error.providerErrorCode) !== null && _c !== void 0 ? _c : 'n/a'}`);
            return new common_1.ServiceUnavailableException((0, api_locale_1.getLocaleText)('oauth.callback.unavailable', locale, 'Unable to complete OAuth authentication right now. Please try again.'));
        }
        this.logger.error(`Unexpected OAuth callback failure: provider=${provider} type=${type} error=${error instanceof Error ? error.message : 'unknown'}`);
        return new common_1.ServiceUnavailableException((0, api_locale_1.getLocaleText)('oauth.callback.unavailable', locale, 'Unable to complete OAuth authentication right now. Please try again.'));
    }
    async handleLogin(res, locale, ipAddress, userAgent, provider, profile) {
        var _a;
        const userAccount = await this.prisma.user_account.findFirst({
            where: {
                provider: String(provider).toLowerCase().replace(/-/g, '_'),
                email: profile.email,
            },
            include: {
                user: true,
            },
        });
        if (!userAccount) {
            const userIdentifier = await this.prisma.user_identifier.findFirst({
                where: {
                    type: 'email',
                    value: profile.email,
                    enabled: true
                },
                include: { user: true },
            });
            if (userIdentifier) {
                return this.handleConnect(res, locale, ipAddress, userAgent, provider, profile, userIdentifier.user_id);
            }
            else {
                return this.handleRegister(res, locale, ipAddress, userAgent, provider, profile);
            }
        }
        else {
            if ((_a = profile.oauth_tokens) === null || _a === void 0 ? void 0 : _a.refresh_token) {
                const encryptedRefreshToken = this.security.encrypt(profile.oauth_tokens.refresh_token);
                const scopes = await this.resolveProfileScopes(provider, profile);
                await this.prisma.user_account.update({
                    where: { id: userAccount.id },
                    data: {
                        refresh_token: Buffer.from(encryptedRefreshToken),
                        token_expires_at: profile.oauth_tokens.expires_in
                            ? new Date(Date.now() + profile.oauth_tokens.expires_in * 1000)
                            : null,
                        scopes,
                    },
                });
            }
            const { accessToken, refreshToken, session } = await this.auth.getAuthenticationPayload(locale, userAccount.user_id, ipAddress, userAgent);
            await this.token.setRefreshTokenCookie(locale, res, refreshToken, session.expires_at);
            return { accessToken };
        }
    }
    async handleRegister(res, locale, ipAddress, userAgent, provider, profile) {
        var _a, _b, _c, _d;
        const existingUser = await this.prisma.user_identifier.findFirst({
            where: {
                type: 'email',
                value: profile.email,
            },
        });
        if (existingUser) {
            throw new common_1.ConflictException('Este email já está sendo usado. Entre em sua conta e utilize a opção "Conectar".');
        }
        const encryptedRefreshToken = profile.oauth_tokens.refresh_token ? this.security.encrypt(profile.oauth_tokens.refresh_token) : null;
        const scopes = await this.resolveProfileScopes(provider, profile);
        const settings = await this.setting.getSettingValues([
            'oauth-role-assignment',
        ]);
        const roles = settings['oauth-role-assignment'] || [];
        const roleIds = this.prisma.role.findMany({
            where: {
                slug: { in: roles }
            },
            select: { id: true }
        });
        const user = await this.prisma.user.create({
            data: {
                name: profile.name,
                role_user: {
                    create: (await roleIds).map((role) => ({ role_id: role.id })),
                },
                user_identifier: {
                    create: {
                        type: 'email',
                        value: profile.email,
                        verified_at: new Date(),
                        enabled: true
                    },
                },
                user_account: {
                    create: {
                        provider: String(provider).toLowerCase().replace(/-/g, '_'),
                        provider_user_id: profile.id,
                        email: profile.email,
                        refresh_token: encryptedRefreshToken ? Buffer.from(encryptedRefreshToken) : null,
                        token_expires_at: ((_a = profile.oauth_tokens) === null || _a === void 0 ? void 0 : _a.expires_in)
                            ? new Date(Date.now() + profile.oauth_tokens.expires_in * 1000)
                            : null,
                        scopes,
                    },
                },
            },
        });
        await this.mail.sendTemplatedMail(locale, {
            email: profile.email,
            slug: 'auth-sign-up',
            variables: { name: profile.name },
        });
        const pictureURL = ((_c = (_b = profile.picture) === null || _b === void 0 ? void 0 : _b.data) === null || _c === void 0 ? void 0 : _c.url) || profile.picture;
        if (pictureURL) {
            const photoId = await this.downloadAndSaveProfilePicture(pictureURL, user.id, (_d = profile.oauth_tokens) === null || _d === void 0 ? void 0 : _d.access_token);
            await this.prisma.user.update({
                where: { id: user.id },
                data: { photo_id: photoId },
            });
        }
        await this.user.registerUserActivity(user.id, 'oauth-register');
        const { accessToken, refreshToken, session } = await this.auth.getAuthenticationPayload(locale, user.id, ipAddress, userAgent);
        await this.token.setRefreshTokenCookie(locale, res, refreshToken, session.expires_at);
        return { accessToken };
    }
    async handleConnect(res, locale, ipAddress, userAgent, provider, profile, userId) {
        var _a, _b, _c, _d, _e;
        const existingAccount = await this.prisma.user_account.findFirst({
            where: {
                user_id: userId,
                provider: String(provider).toLowerCase().replace(/-/g, '_'),
            },
        });
        if (existingAccount) {
            throw new common_1.BadRequestException(`Usuário já conectado com o ${provider}.`);
        }
        const encryptedRefreshToken = profile.oauth_tokens.refresh_token ? this.security.encrypt(profile.oauth_tokens.refresh_token) : null;
        const scopes = await this.resolveProfileScopes(provider, profile);
        await this.prisma.user_account.create({
            data: {
                user_id: userId,
                provider: String(provider).toLowerCase().replace(/-/g, '_'),
                provider_user_id: profile.id,
                email: profile.email,
                refresh_token: encryptedRefreshToken ? Buffer.from(encryptedRefreshToken) : null,
                token_expires_at: ((_a = profile.oauth_tokens) === null || _a === void 0 ? void 0 : _a.expires_in)
                    ? new Date(Date.now() + profile.oauth_tokens.expires_in * 1000)
                    : null,
                scopes,
            },
        });
        const user = await this.prisma.user.findUnique({ where: { id: userId } });
        const updateData = {};
        const pictureURL = ((_c = (_b = profile.picture) === null || _b === void 0 ? void 0 : _b.data) === null || _c === void 0 ? void 0 : _c.url) || profile.picture;
        if (pictureURL) {
            const photoId = await this.downloadAndSaveProfilePicture(pictureURL, userId, (_d = profile.oauth_tokens) === null || _d === void 0 ? void 0 : _d.access_token);
            if (photoId) {
                updateData.photo_id = photoId;
            }
        }
        if (profile.name && profile.name.length > (((_e = user.name) === null || _e === void 0 ? void 0 : _e.length) || 0)) {
            updateData.name = profile.name;
        }
        if (Object.keys(updateData).length > 0) {
            await this.prisma.user.update({
                where: { id: userId },
                data: updateData,
            });
        }
        await this.user.registerUserActivity(userId, 'oauth-connect');
        const { accessToken, refreshToken, session } = await this.auth.getAuthenticationPayload(locale, user.id, ipAddress, userAgent);
        await this.token.setRefreshTokenCookie(locale, res, refreshToken, session.expires_at);
        return { accessToken };
    }
    async handleDisconnect(res, locale, ipAddress, userAgent, provider, email) {
        const userAccount = await this.prisma.user_account.findFirst({
            where: {
                email,
                provider: String(provider).toLowerCase().replace(/-/g, '_'),
            },
            include: { user: true }
        });
        if (!userAccount) {
            throw new common_1.NotFoundException(`Usuário não conectado ao ${provider}.`);
        }
        await this.prisma.user_account.delete({
            where: { id: userAccount.id },
        });
        await this.user.registerUserActivity(userAccount.user.id, 'oauth-disconnect');
        const { accessToken, refreshToken, session } = await this.auth.getAuthenticationPayload(locale, userAccount.user.id, ipAddress, userAgent);
        await this.token.setRefreshTokenCookie(locale, res, refreshToken, session.expires_at);
        return { accessToken };
    }
    async downloadAndSaveProfilePicture(url, userId, accessToken) {
        if (!url)
            return null;
        const headers = {};
        if (accessToken) {
            headers['Authorization'] = `Bearer ${accessToken}`;
        }
        const response = await fetch(url, { headers });
        if (!response.ok) {
            console.warn(`Failed to download profile picture from ${url}: ${response.statusText}`);
            return null;
        }
        const buffer = Buffer.from(await response.arrayBuffer());
        const file = {
            originalname: `profile_${userId}.jpg`,
            mimetype: response.headers.get('content-type') || 'image/jpeg',
            buffer,
            size: buffer.length,
        };
        const savedFile = await this.file.upload(user_constants_1.USER_AVATAR_UPLOAD_DESTINATION, file);
        return savedFile.id;
    }
};
exports.OAuthService = OAuthService;
exports.OAuthService = OAuthService = OAuthService_1 = __decorate([
    (0, common_1.Injectable)(),
    __param(6, (0, common_1.Inject)((0, common_1.forwardRef)(() => auth_service_1.AuthService))),
    __param(13, (0, common_1.Inject)((0, common_1.forwardRef)(() => user_service_1.UserService))),
    __metadata("design:paramtypes", [google_provider_1.GoogleProvider,
        facebook_provider_1.FacebookProvider,
        microsoft_provider_1.MicrosoftProvider,
        github_provider_1.GithubProvider,
        microsoft_entra_id_provider_1.MicrosoftEntraIdProvider,
        oauth_callback_coordinator_service_1.OAuthCallbackCoordinatorService,
        auth_service_1.AuthService,
        file_service_1.FileService,
        mail_service_1.MailService,
        security_service_1.SecurityService,
        api_prisma_1.PrismaService,
        token_service_1.TokenService,
        setting_service_1.SettingService,
        user_service_1.UserService])
], OAuthService);
//# sourceMappingURL=oauth.service.js.map