import { TFANewTOTP, Server, TFAOption, UserRegisterInfo, TFAWebAuthRegister } from "@hibas123/openauth-internalapi";
import type { SessionContext } from "../index.js";
import TwoFactorModel, { ITOTP, IWebAuthn, TFATypes } from "../../../models/twofactor.js";
import moment = require("moment");
import * as speakeasy from "speakeasy";
import * as qrcode from "qrcode";
import config from "../../../config.js";
import { generateRegistrationOptions, verifyRegistrationResponse } from '@simplewebauthn/server';
// import type { RegistrationResponseJSON } from '@simplewebauthn/typescript-types';
import Logging from "@hibas123/nodelogging";
import { Binary } from "mongodb";
import { RequireLogin } from "../../../helper/login.js";


export default class TFAService extends Server.TFAService<SessionContext> {
   @RequireLogin()
   AddBackupCodes(name: string, ctx: SessionContext): Promise<string[]> {
      throw new Error("Method not implemented.");
   }

   @RequireLogin()
   RemoveBackupCodes(id: string, ctx: SessionContext): Promise<void> {
      throw new Error("Method not implemented.");
   }

   @RequireLogin()
   async GetOptions(ctx: SessionContext): Promise<TFAOption[]> {
      let twofactor = await TwoFactorModel.find({ user: ctx.user._id, valid: true });
      let expired = twofactor.filter((e) =>
         e.expires ? moment().isAfter(moment(e.expires)) : false
      );
      await Promise.all(
         expired.map((e) => {
            e.valid = false;
            return TwoFactorModel.save(e);
         })
      );

      twofactor = twofactor.filter((e) => e.valid);
      let tfa = twofactor.map<TFAOption>((e) => {
         return {
            id: e._id.toString(),
            name: e.name,
            tfatype: e.type as number,
            expires: e.expires?.valueOf()
         };
      });

      return tfa;
   }

   @RequireLogin()
   async Delete(id: string, ctx: SessionContext): Promise<void> {
      let twofactor = await TwoFactorModel.findById(id);
      if (!twofactor || !twofactor.user.equals(ctx.user._id))
         throw new Error("Invalid ID");

      twofactor.valid = false;
      await TwoFactorModel.save(twofactor);
   }

   @RequireLogin()
   async AddTOTP(name: string, ctx: SessionContext): Promise<TFANewTOTP> {
      //Generating new
      let secret = speakeasy.generateSecret({
         name: config.core.name,
         issuer: config.core.name,
         otpauth_url: true
      });

      let twofactor = TwoFactorModel.new(<ITOTP>{
         name: name,
         user: ctx.user._id,
         type: TFATypes.TOTP,
         valid: false,
         data: secret.base32,
      });

      let dataurl = await qrcode.toDataURL(secret.otpauth_url);
      await TwoFactorModel.save(twofactor);

      return {
         id: twofactor._id.toString(),
         qr: dataurl,
         secret: secret.base32
      }
   }

   @RequireLogin()
   async VerifyTOTP(id: string, code: string, ctx: SessionContext): Promise<void> {
      let twofactor = await TwoFactorModel.findById(id);
      if (!twofactor || !twofactor.user.equals(ctx.user._id))
         throw new Error("Invalid ID");

      let verified = speakeasy.totp.verify({
         secret: twofactor.data,
         encoding: "base32",
         token: code,
      });

      if (!verified) throw new Error("Invalid code");

      twofactor.valid = true;
      twofactor.expires = undefined;

      await TwoFactorModel.save(twofactor);
   }

   @RequireLogin()
   async AddWebauthn(name: string, ctx: SessionContext): Promise<TFAWebAuthRegister> {
      // TODO: Get already registered options

      const rpID = new URL(config.core.url).hostname;
      const options = await generateRegistrationOptions({
         rpName: config.core.name,
         rpID,
         userID: Buffer.from(ctx.user.uid, "utf-8"),
         userName: ctx.user.username,
         attestationType: 'direct',
         userDisplayName: ctx.user.name,
         excludeCredentials: [],
         authenticatorSelection: {
            userVerification: "required",
            requireResidentKey: false,
            residentKey: "discouraged",
            authenticatorAttachment: "cross-platform"
         }
      })


      const twofactor = TwoFactorModel.new({
         name,
         type: TFATypes.WEBAUTHN,
         user: ctx.user._id,
         valid: false,
         data: {
            challenge: options.challenge
         }
      });

      await TwoFactorModel.save(twofactor);

      Logging.debug(twofactor);

      return {
         id: twofactor._id.toString(),
         challenge: JSON.stringify(options)
      };
   }

   @RequireLogin()
   async VerifyWebAuthn(id: string, registration: string, ctx: SessionContext): Promise<void> {
      let twofactor = await TwoFactorModel.findById(id) as IWebAuthn;
      if (!twofactor || !twofactor.user.equals(ctx.user._id))
         throw new Error("Invalid ID");

      const rpID = new URL(config.core.url).hostname;

      const response = JSON.parse(registration); // as RegistrationResponseJSON;

      let verification = await verifyRegistrationResponse({
         response,
         expectedChallenge: twofactor.data.challenge,
         expectedOrigin: config.core.url,
         expectedRPID: rpID,
         requireUserVerification: true,
      });

      if (verification.verified) {
         const { credential, } = verification.registrationInfo;

         //TODO: Check if already registered!
         // TwoFactorModel.find({
         //    data: {
         //    }
         // })

         twofactor.data = {
            device: {
               counter: credential.counter,
               credentialPublicKey: new Binary(credential.publicKey),
               credentialID: credential.id,
               // counter: verification.registrationInfo.counter,
               transports: response.response.transports as any[],
            }
         }

         twofactor.valid = true;

         await TwoFactorModel.save(twofactor);
      } else {
         throw new Error("Invalid response");
      }
   }
}
