import type { User } from "bknd";
import type { Authenticator } from "auth/authenticate/Authenticator";
import { InvalidCredentialsException } from "auth/errors";
import { hash, $console, s, parse, jsc, describeRoute } from "bknd/utils";
import { Hono } from "hono";
import { compare as bcryptCompare, genSalt as bcryptGenSalt, hash as bcryptHash } from "bcryptjs";
import { AuthStrategy } from "./Strategy";

const schema = s
   .object({
      hashing: s.string({ enum: ["plain", "sha256", "bcrypt"], default: "sha256" }),
      rounds: s.number({ minimum: 1, maximum: 10 }).optional(),
      minLength: s.number({ default: 8, minimum: 1 }).optional(),
   })
   .strict();

export type PasswordStrategyOptions = s.Static<typeof schema>;

export class PasswordStrategy extends AuthStrategy<typeof schema> {
   constructor(config: Partial<PasswordStrategyOptions> = {}) {
      super(config as any, "password", "password", "form");

      this.registerAction("create", this.getPayloadSchema(), async ({ password, ...input }) => {
         return {
            ...input,
            strategy_value: await this.hash(password),
         };
      });
   }

   getSchema() {
      return schema;
   }

   private getPayloadSchema() {
      return s.object({
         email: s.string({
            format: "email",
         }),
         password: s.string({
            minLength: this.config.minLength,
         }),
      });
   }

   async hash(password: string) {
      switch (this.config.hashing) {
         case "sha256":
            return hash.sha256(password);
         case "bcrypt": {
            const salt = await bcryptGenSalt(this.config.rounds ?? 4);
            return bcryptHash(password, salt);
         }
         default:
            return password;
      }
   }

   async compare(actual: string, compare: string): Promise<boolean> {
      switch (this.config.hashing) {
         case "sha256": {
            const compareHashed = await this.hash(compare);
            return actual === compareHashed;
         }
         case "bcrypt":
            return await bcryptCompare(compare, actual);
      }

      return actual === compare;
   }

   verify(password: string) {
      return async (user: User) => {
         if (!user || !user.strategy_value) {
            throw new InvalidCredentialsException();
         }

         if (!this.getPayloadSchema().properties.password.validate(password).valid) {
            $console.debug("PasswordStrategy: Invalid password", password);
            throw new InvalidCredentialsException();
         }

         const compare = await this.compare(user.strategy_value, password);
         if (compare !== true) {
            throw new InvalidCredentialsException();
         }
      };
   }

   getController(authenticator: Authenticator, opts: { allow_register?: boolean }): Hono<any> {
      const hono = new Hono();
      const redirectQuerySchema = s.object({
         redirect: s.string().optional(),
      });
      const payloadSchema = this.getPayloadSchema();

      hono.post(
         "/login",
         describeRoute({
            summary: "Login with email and password",
            tags: ["auth"],
         }),
         jsc("query", redirectQuerySchema),
         async (c) => {
            try {
               const body = parse(payloadSchema, await authenticator.getBody(c), {
                  onError: (errors) => {
                     $console.error("Invalid login payload", [...errors]);
                     throw new InvalidCredentialsException();
                  },
               });
               const { redirect } = c.req.valid("query");

               return await authenticator.resolveLogin(c, this, body, this.verify(body.password), {
                  redirect,
               });
            } catch (e) {
               return authenticator.respondWithError(c, e as any);
            }
         },
      );

      if (opts.allow_register) {
         hono.post(
            "/register",
            describeRoute({
               summary: "Register a new user with email and password",
               tags: ["auth"],
            }),
            jsc("query", redirectQuerySchema),
            async (c) => {
               try {
                  const { redirect } = c.req.valid("query");
                  const { password, email, ...body } = parse(
                     payloadSchema,
                     await authenticator.getBody(c),
                     {
                        onError: (errors) => {
                           $console.error("Invalid register payload", [...errors]);
                           new InvalidCredentialsException();
                        },
                     },
                  );

                  const profile = {
                     ...body,
                     email,
                     strategy_value: await this.hash(password),
                  };

                  return await authenticator.resolveRegister(c, this, profile, async () => void 0, {
                     redirect,
                  });
               } catch (e) {
                  return authenticator.respondWithError(c, e as any);
               }
            },
         );
      }

      return hono;
   }
}
