import { errAsync, okAsync, ResultAsync } from "neverthrow";
import { FlowExecCtx } from "@core/flow.execution.context";
import { UserRepository } from "@domains/user/repository";
import { getRedisInstance, Redis } from "@pkg/keystore";
import { TwofaRepository } from "./repository";
import { logDomainEvent } from "@pkg/logger";
import { auth } from "../auth/config.base";
import type { TwoFaSession } from "./data";
import { User } from "@domains/user/data";
import { settings } from "@core/settings";
import { type Err } from "@pkg/result";
import { twofaErrors } from "./errors";
import { db } from "@pkg/db";

export class TwofaController {
    constructor(
        private twofaRepo: TwofaRepository,
        private userRepo: UserRepository,
        private store: Redis,
        private secret: string,
    ) {}

    checkTotp(secret: string, code: string) {
        return this.twofaRepo.checkTotp(secret, code);
    }

    is2faEnabled(fctx: FlowExecCtx, userId: string) {
        return this.twofaRepo
            .getUsers2FAInfo(fctx, userId, true)
            .map((data) => !!data)
            .orElse(() => okAsync(false));
    }

    isUserBanned(fctx: FlowExecCtx, userId: string) {
        return this.userRepo.isUserBanned(fctx, userId).orElse((error) => {
            logDomainEvent({
                level: "error",
                event: "security.twofa.user_ban_check.failed",
                fctx,
                error,
                meta: { userId },
            });
            return okAsync(false);
        });
    }

    setup2FA(fctx: FlowExecCtx, user: User) {
        return this.is2faEnabled(fctx, user.id)
            .andThen((enabled) =>
                enabled
                    ? errAsync(twofaErrors.alreadyEnabled(fctx))
                    : this.twofaRepo.setup(fctx, user.id, this.secret),
            )
            .map((secret) => {
                const appName = settings.appName;
                const totpUri = `otpauth://totp/${appName}:${user.email}?secret=${secret}&issuer=${appName}`;
                return { totpURI: totpUri, secret };
            });
    }

    verifyAndEnable2FA(
        fctx: FlowExecCtx,
        user: User,
        code: string,
        headers: Headers,
    ) {
        const startedAt = Date.now();
        logDomainEvent({
            event: "security.twofa.verify_and_enable.started",
            fctx,
            meta: { userId: user.id },
        });

        return this.is2faEnabled(fctx, user.id)
            .andThen((enabled) => {
                if (enabled) {
                    logDomainEvent({
                        level: "warn",
                        event: "security.twofa.verify_and_enable.failed",
                        fctx,
                        durationMs: Date.now() - startedAt,
                        error: {
                            code: "ALREADY_ENABLED",
                            message: "2FA already enabled",
                        },
                        meta: { userId: user.id },
                    });
                    return errAsync(twofaErrors.alreadyEnabled(fctx));
                }
                return okAsync(undefined);
            })
            .andThen(() =>
                this.twofaRepo.verifyAndEnable2FA(fctx, user.id, code),
            )
            .andThen((verified) => {
                if (verified) {
                    return ResultAsync.combine([
                        ResultAsync.fromPromise(
                            auth.api.revokeOtherSessions({ headers }),
                            () => twofaErrors.revokeSessionsFailed(fctx),
                        ),
                        this.userRepo.updateLastVerified2FaAtToNow(
                            fctx,
                            user.id,
                        ),
                    ]).map(() => {
                        logDomainEvent({
                            event: "security.twofa.verify_and_enable.succeeded",
                            fctx,
                            durationMs: Date.now() - startedAt,
                            meta: { userId: user.id },
                        });
                        return true;
                    });
                }
                logDomainEvent({
                    level: "warn",
                    event: "security.twofa.verify_and_enable.failed",
                    fctx,
                    durationMs: Date.now() - startedAt,
                    error: {
                        code: "INVALID_CODE",
                        message: "2FA code verification failed",
                    },
                    meta: { userId: user.id },
                });
                return okAsync(verified);
            });
    }

    disable(fctx: FlowExecCtx, user: User, code: string) {
        return this.is2faEnabled(fctx, user.id)
            .andThen((enabled) => {
                if (!enabled) {
                    return errAsync(twofaErrors.notEnabled(fctx));
                }
                return okAsync(undefined);
            })
            .andThen(() => this.twofaRepo.get2FASecret(fctx, user.id))
            .andThen((secret) => {
                if (!secret) {
                    return errAsync(twofaErrors.invalidSetup(fctx));
                }
                if (!this.checkTotp(secret, code)) {
                    return errAsync(twofaErrors.invalidCode(fctx));
                }
                return okAsync(undefined);
            })
            .andThen(() => this.twofaRepo.disable(fctx, user.id));
    }

    generateBackupCodes(fctx: FlowExecCtx, user: User) {
        return this.is2faEnabled(fctx, user.id)
            .andThen((enabled) => {
                if (!enabled) {
                    return errAsync(twofaErrors.notEnabled(fctx));
                }
                return okAsync(undefined);
            })
            .andThen(() => this.twofaRepo.generateBackupCodes(fctx, user.id));
    }

    requiresInitialVerification(
        fctx: FlowExecCtx,
        user: User,
        sessionId: string,
    ) {
        return this.is2faEnabled(fctx, user.id).andThen((enabled) => {
            if (!enabled) {
                return okAsync(false);
            }

            return ResultAsync.fromPromise(
                this.store.get(`initial_2fa_completed:${sessionId}`),
                () => null,
            )
                .map((completed) => !completed && completed !== "0")
                .orElse(() => okAsync(true));
        });
    }

    requiresSensitiveActionVerification(fctx: FlowExecCtx, user: User) {
        return this.is2faEnabled(fctx, user.id).andThen((enabled) => {
            if (!enabled) {
                return okAsync(false);
            }

            if (!user.last2FAVerifiedAt) {
                return okAsync(true);
            }

            const requiredHours = settings.twofaRequiredHours || 24;
            const verificationAge =
                Date.now() - user.last2FAVerifiedAt.getTime();
            const maxAge = requiredHours * 60 * 60 * 1000;

            return okAsync(verificationAge > maxAge);
        });
    }

    markInitialVerificationComplete(sessionId: string) {
        return ResultAsync.fromPromise(
            this.store.setex(
                `initial_2fa_completed:${sessionId}`,
                60 * 60 * 24 * 7,
                "true",
            ),
            () => null,
        )
            .map(() => undefined)
            .orElse((error) => {
                logDomainEvent({
                    level: "error",
                    event: "security.twofa.mark_initial_verification.failed",
                    fctx: { flowId: crypto.randomUUID() },
                    error,
                });
                return okAsync(undefined);
            });
    }

    startVerification(
        fctx: FlowExecCtx,
        params: {
            userId: string;
            sessionId: string;
            ipAddress?: string;
            userAgent?: string;
        },
    ) {
        return this.twofaRepo.createSession(fctx, params).map((session) => ({
            verificationToken: session.verificationToken,
        }));
    }

    private validateSession(fctx: FlowExecCtx, session: TwoFaSession) {
        if (session.status !== "pending") {
            return errAsync(twofaErrors.sessionNotActive(fctx));
        }

        if (session.expiresAt < new Date()) {
            return this.twofaRepo
                .updateSession(fctx, session.id, { status: "expired" })
                .andThen(() => errAsync(twofaErrors.sessionExpired(fctx)));
        }

        return okAsync(session);
    }

    private handleMaxAttempts(
        fctx: FlowExecCtx,
        session: TwoFaSession,
        userId: string,
    ) {
        const banExpiresAt = new Date();
        banExpiresAt.setHours(banExpiresAt.getHours() + 1);

        return this.twofaRepo
            .updateSession(fctx, session.id, { status: "failed" })
            .andThen(() =>
                this.userRepo.banUser(
                    fctx,
                    userId,
                    "Too many failed 2FA verification attempts",
                    banExpiresAt,
                ),
            )
            .andThen(() => errAsync(twofaErrors.tooManyAttempts(fctx)));
    }

    private checkAttemptsLimit(
        fctx: FlowExecCtx,
        session: TwoFaSession,
        userId: string,
    ) {
        if (session.attempts >= session.maxAttempts) {
            return this.handleMaxAttempts(fctx, session, userId);
        }
        return okAsync(session);
    }

    private checkCodeReplay(
        fctx: FlowExecCtx,
        session: TwoFaSession,
        code: string,
    ): ResultAsync<TwoFaSession, Err> {
        if (session.codeUsed === code) {
            return this.twofaRepo
                .incrementAttempts(fctx, session.id)
                .andThen(() => errAsync(twofaErrors.codeReplay(fctx)));
        }
        return okAsync(session);
    }

    private verifyTotpCode(
        fctx: FlowExecCtx,
        session: TwoFaSession,
        userId: string,
        code: string,
    ) {
        return this.twofaRepo.get2FASecret(fctx, userId).andThen((secret) => {
            if (!secret) {
                return errAsync(twofaErrors.invalidSetup(fctx));
            }

            if (!this.checkTotp(secret, code)) {
                return this.twofaRepo
                    .incrementAttempts(fctx, session.id)
                    .andThen(() => errAsync(twofaErrors.invalidCode(fctx)));
            }

            return okAsync(session);
        });
    }

    private completeVerification(
        fctx: FlowExecCtx,
        session: TwoFaSession,
        userId: string,
        code: string,
    ) {
        return this.twofaRepo
            .updateSession(fctx, session.id, {
                status: "verified",
                verifiedAt: new Date(),
                codeUsed: code,
            })
            .andThen(() =>
                ResultAsync.combine([
                    this.userRepo.updateLastVerified2FaAtToNow(fctx, userId),
                    this.markInitialVerificationComplete(session.sessionId),
                ]),
            )
            .map(() => undefined);
    }

    verifyCode(
        fctx: FlowExecCtx,
        params: { verificationSessToken: string; code: string },
        user?: User,
    ) {
        if (!user) {
            return errAsync(twofaErrors.userNotFound(fctx));
        }

        return this.is2faEnabled(fctx, user.id)
            .andThen((enabled) => {
                if (!enabled) {
                    return errAsync(
                        twofaErrors.notEnabledForVerification(fctx),
                    );
                }
                return okAsync(undefined);
            })
            .andThen(() =>
                this.twofaRepo.getSessionByToken(
                    fctx,
                    params.verificationSessToken,
                ),
            )
            .andThen((session) => {
                if (!session) {
                    return errAsync(twofaErrors.sessionNotFound(fctx));
                }
                return okAsync(session);
            })
            .andThen((session) => this.validateSession(fctx, session))
            .andThen((session) =>
                this.checkAttemptsLimit(fctx, session, user.id),
            )
            .andThen((session) =>
                this.checkCodeReplay(fctx, session, params.code),
            )
            .andThen((session) =>
                this.verifyTotpCode(fctx, session, user.id, params.code),
            )
            .andThen((session) =>
                this.completeVerification(fctx, session, user.id, params.code),
            )
            .map(() => ({ success: true }));
    }

    cleanupExpiredSessions(fctx: FlowExecCtx) {
        return this.twofaRepo.cleanupExpiredSessions(fctx);
    }
}

export function getTwofaController() {
    const _redis = getRedisInstance();
    return new TwofaController(
        new TwofaRepository(db, _redis),
        new UserRepository(db),
        _redis,
        settings.twoFaSecret,
    );
}
