import { join } from "jsr:@std/path";
import { createMiddleware } from "jsr:@hono/hono/factory";
import { HTTPException } from "jsr:@hono/hono/http-exception";
import { Env, Hono } from "jsr:@hono/hono";
import { md5 } from "jsr:@takker/md5";
import { encodeHex } from "jsr:@std/encoding@1/hex";
import { getCookie, setCookie } from "jsr:@hono/hono/cookie";

type AccessCallback = (r: Result) => string;
type Role = "admin" | "user";

export class LoginException extends HTTPException {
  constructor(message: string) {
    super(404, { message });
  }
}

export class RegisterException extends HTTPException {
  constructor(message: string) {
    super(404, { message });
  }
}

export interface User {
  name: string;
  password: string;
  role: Role;
  created: string;
}

export interface Result {
  title: string;
  description?: string;
}

export class Access {
  kv: Deno.Kv;
  loginSuccess = (_user:User)=>"/";
  registerSuccess = (_user:User)=>"/";

  constructor(kv: Deno.Kv) {
    this.kv = kv;
  }

  static async initialize<T extends Env>(app: Hono<T>, path: string) {
    const kv_path = join(path);
    const kv = await Deno.openKv(kv_path);

    const result = new Access(kv);
    result.setup(app);

    return result;
  }

  setup<T extends Env>(app: Hono<T>) {
    const kv = this.kv;

    app.get("/login", async (c) => {
      // Should create error.
      const user_name = c.req.query("user") ?? "";
      const password = encodeHex(md5(c.req.query("password") ?? ""));
      const user = await getUser(kv, user_name);
      if (user === null) {
        throw new HTTPException(404, { message: "No user exist." });
      } else if (user.password === password) {
        const token = await createLogin(kv, user);
        setCookie(c, "token", token);
        return c.redirect(this.loginSuccess(user));
      } else {
        const msg = "Wrong password or user name.";
        throw new LoginException(msg);
      }
    });

    app.get("/register", async (c) => {
      // Should create error.
      const user_name = c.req.query("user") ?? "";
      const password = encodeHex(md5(c.req.query("password") ?? ""));
      const user = await getUser(kv, user_name);
      if (user === null) {
        const user:User = {
          name: user_name,
          password: password,
          role: "user",
          created: toDay(),
        };
        setUser(kv, user_name, user);
        return c.redirect(this.registerSuccess(user));
      } else {
        const msg = "Allready a user with that name.";
        throw new RegisterException(msg);
      }
    });
  }

  setLoginSuccess(redirect = (_user:User)=>"/") {
    this.loginSuccess = redirect;
  }

  setRegisterSucces(redirect = (_user:User)=>"/") {
    this.registerSuccess = redirect;
  }

  check(..._roles: Role[]) {
    return createMiddleware(async (c, next) => {
      const token = await getCookie(c, "token") ?? "";
      if (!token) {
        throw new HTTPException(404, { message: "No token as a cookie" });
      }
      const user = await checkLogin(this.kv, token);
      if (!user) {
        throw new HTTPException(404, { message: "Bad token" });
      }
      c.set("user", user)
      await next();
    });
  }
}

async function getUser(kv: Deno.Kv, user_name: string): Promise<User | null> {
  const keys = ["user", user_name];
  const user = (await kv.get<User>(keys)).value;
  return user;
}

function setUser(kv: Deno.Kv, user_name: string, user: User) {
  const keys = ["user", user_name];
  kv.set(keys, user);
}

function createLogin(kv: Deno.Kv, user: User) {
  const token = crypto.randomUUID();
  const keys = ["login", token];
  kv.set(keys, user);
  return token;
}

async function checkLogin(kv: Deno.Kv, token: string) {
  const keys = ["login", token];
  const user = (await kv.get<User>(keys)).value;
  return user;
}

function toDay() {
  const today = new Date();
  const yyyy = today.getFullYear();
  const mm = String(today.getMonth() + 1).padStart(2, "0");
  const dd = String(today.getDate()).padStart(2, "0");
  const hh = String(today.getHours()).padStart(2, "0");
  const min = String(today.getMinutes()).padStart(2, "0");

  const formattedDateTime = `${yyyy}-${mm}-${dd} ${hh}:${min}`;
  return formattedDateTime;
}
