import { Context, Hono } from "@hono/hono";
import { serveStatic } from "@hono/hono/deno";
import { createMiddleware } from "@hono/hono/factory";
import { getCookie, setCookie } from "@hono/hono/cookie";
import { dirname, join } from "@std/path";
import { encodeHex } from "@std/encoding/hex";
import Mustache from "mustache";
import { Input } from "@hono/hono/types";

type MyEnv = { Variables: { user: UserData } };

/**
 * Create a server object
 */
export function created() {
  console.log("Lib version: %c0.0.8 - 2026-05-25", "color: green");
  const app = new Hono<MyEnv>();
  app.use(logger);
  app.use(setUser());
  app.use("/*", serveStatic({ root: "./public" }));
  return app;
}

/**
 * Start a server
 */
// deno-lint-ignore no-explicit-any
export function start(app: any) {
  app.get("*", serveStatic({ path: "./public/404.html" }));
  Deno.serve(app.fetch);
}

/**
 * Apply a js object on on a template file on path. If path do not exist
 * use alt.
 */
// deno-lint-ignore no-explicit-any
export async function template(c: any, data: any, path: string, alt = "") {
  const source = import.meta.dirname ?? "";
  const template_path = join(source, "../template/", path);
  let tp;
  try {
    tp = await Deno.readTextFile(template_path);
  } catch {
    tp = alt;
  }
  const page = Mustache.render(tp, data);
  return c.html(page);
}

/**
 * Apply a js object on on a text
 */
// deno-lint-ignore no-explicit-any
export async function render(c: any, data: any, text: string) {
  const page = await Mustache.render(text, data);
  return c.html(page);
}

interface MetaData {
  __path: string;
}

/**
 * Load data from a file.
 */
export async function load<T extends object>(
  path: string,
  def: T,
): Promise<T & MetaData> {
  const src_dir = import.meta.dirname ?? "./";
  const data_path = join(src_dir, "..", "data", path + ".json");
  try {
    const data_string = await Deno.readTextFile(data_path);
    const data = JSON.parse(data_string);
    data.__path = data_path;
    return await data as T & MetaData;
  } catch {
    return await Object.assign(def, { __path: data_path });
  }
}

/**
 * Save data loaded by the load method back to a file.
 */
export async function save<T>(data: T & MetaData): Promise<boolean> {
  const data_string = JSON.stringify(data, null, 4);
  try {
    await Deno.writeTextFile(data.__path, data_string);
  } catch {
    try {
      const data_dir = dirname(data.__path);
      await Deno.mkdir(data_dir, { recursive: true });
      await Deno.writeTextFile(data.__path, data_string);
    } catch {
      return false;
    }
  }
  return true;
}

/**
 * A middleware for logging connections
 */
const logger = createMiddleware(async (c, next) => {
  const datetime = new Date();
  const hours = datetime.getHours();
  const minutes = datetime.getMinutes();
  const seconds = datetime.getSeconds();
  console.log(
    "%c[" +
      (Number(hours) > 9 ? hours : "0" + hours) +
      ":" +
      (Number(minutes) > 9 ? minutes : "0" + minutes) +
      ":" +
      (Number(seconds) > 9 ? seconds : "0" + seconds) +
      "]" +
      "%c # %c" +
      c.req.url,
    "color: green",
    "",
    "color: blue",
  );
  await next();
});

// Role a user kan have on the site.
export type Role = "admin" | "member";

// Path to where user data is saved.
const user_data_path = "user";
const waiting_list_path = "waiting";

type Index = { [index: string]: number };
type UserDatabas = UserData[];

interface UserData {
  alias: string;
  password: string;
  role: Role;
  id: number;
}

const token_index: Index = {};

const default_waiting_list: UserDatabas = [];

const default_value: UserDatabas = [
  {
    alias: "admin",
    password: await hash("abc123"),
    role: "admin",
    id: 0,
  },
  {
    alias: "test",
    password: await hash("abc"),
    role: "member",
    id: 1,
  },
];

export const waiting_list = await load(waiting_list_path, default_waiting_list);
await save(waiting_list);
const storage = await load(user_data_path, default_value);
const userdatabas_alias_index = index(storage, "alias");
await save(storage);

/**
 * Midelware for locking sites
 */
export function setUser() {
  return createMiddleware(async (c, next) => {
    const token = getCookie(c, "login");
    if (typeof token === "string") {
      const user = storage[token_index[token]];
      if (user !== undefined) {
        c.set("user", user);
      }
    }
    return await next();
  });
}

/**
 * Midelware for locking sites
 */
export function lock(...roles: Role[]) {
  return createMiddleware(async (c, next) => {
    const tooken_data = c.get("user");
    if (tooken_data !== undefined) {
      const user = tooken_data.alias;
      if (roles.indexOf(tooken_data.role) !== -1) {
        return await next();
      } else {
        c.status(403);
        console.error("Not allowed for user: " + user);
        return template(c, { user: user }, "not-allowed.tp.html");
      }
    }
    c.status(401);
    const respons = "Not logged in!";
    console.error(respons);
    return template(c, {}, "not-logged-in.tp.html", respons);
  });
}

/**
 * A function for login ing a user.
 */
export async function login<T extends Input>(c: Context<MyEnv, string, T>) {
  const query = c.req.query() ?? {};
  const name = query.user;
  const password = query.password;
  if (
    typeof name === "string" &&
    typeof password === "string"
  ) {
    const md5 = await hash(password);
    const user = userdatabas_alias_index[name];
    if (user !== undefined) {
      if (user.password === md5) {
        const token = crypto.randomUUID();
        token_index[token] = userdatabas_alias_index[name].id;
        setCookie(c, "login", token, { maxAge: 10000000 });
        c.status(200);
        console.log("You are logged in as " + name + "!");
        return c.redirect("./");
      } else {
        c.status(403);
        const respons = "Wrong password for " + name + "!";
        console.error(respons);
        return template(c, { user: name }, "wrong-password.tp.html", respons);
      }
    }
  }
  c.status(403);
  const respons = "No password or user info!";
  console.error("No password or user info!");
  return template(c, { user: name }, "no-password-user.tp.html", respons);
}

/**
 * A function for login ing a user.
 */
export async function register<T extends Input>(
  c: Context<MyEnv, string, T>,
) {
  const query = c.req.query() ?? {};
  const alias = query.user;
  const password = query.password;
  // deno-lint-ignore no-explicit-any
  const role = (query.role ?? "member") as any;
  await addOnWaitingList(alias, role, password);
  return c.redirect("./");
}

/**
 * A function for accepting a user.
 */
export async function accept<T extends Input>(c: Context<MyEnv, string, T>) {
  const query = c.req.query() ?? {};
  const index = query.id;
  // deno-lint-ignore no-explicit-any
  const user = waiting_list[index as any];
  // deno-lint-ignore no-explicit-any
  delete waiting_list[index as any];
  await addUser(user.alias, user.role, user.password);
  save(waiting_list);
  return c.redirect("./");
}

/**
 * A function for register a user.
 */
export async function addOnWaitingList(
  alias: string,
  role: Role,
  password: string,
) {
  const id = waiting_list.length;
  const new_user = {
    alias,
    password: await hash(password),
    role,
    id,
  };

  waiting_list.push(new_user);
  await save(waiting_list);
}

/**
 * A function for change a password by a user.
 */
export async function password<T extends Input>(
  c: Context<MyEnv, string, T>,
) {
  const query = await c.req.query() ?? {};
  const alias = query.user;
  const old = query.old;
  const password = query.password;
  const password2 = query.password2;
  const user = userdatabas_alias_index[alias];
  if (user.password === await hash(old)) {
    if (password === password2) {
      console.log(`Change password for ${user.alias}`);
      user.password = await hash(password);
      save(storage);
    }
  }

  return c.redirect("./");
}

/**
 * A function for change a password for a user.
 */
export async function changePassword<T extends Input>(
  c: Context<MyEnv, string, T>,
) {
  const query = await c.req.query() ?? {};
  const index = query.id;
  const password = query.password;
  // deno-lint-ignore no-explicit-any
  const user = storage[index as any];
  console.log(`Change password for ${user.alias}`);
  user.password = await hash(password);
  save(storage);
  return c.redirect("./");
}

/**
 * A function for change a role for a user.
 */
export async function changeRole<T extends Input>(
  c: Context<MyEnv, string, T>,
) {
  const query = await c.req.query() ?? {};
  const index = query.id;
  const role = query.role as Role;
  // deno-lint-ignore no-explicit-any
  const user = storage[index as any];
  console.log(`Change ${user.alias} to role: ${role}`);
  user.role = role;
  save(storage);
  return c.redirect("./");
}

/**
 * A function for removing a user.
 */
export async function remove<T extends Input>(c: Context<MyEnv, string, T>) {
  const query = await c.req.query() ?? {};
  const index = Number(query.id);
  const confirm = query.confirm;
  if (confirm === "y" && index > 0) {
    // deno-lint-ignore no-explicit-any
    const user = storage[index as any];
    console.log(`Remove ${user.alias}`);
    // deno-lint-ignore no-explicit-any
    delete storage[index as any];
    save(storage);
  }
  return c.redirect("./");
}

/**
 * A function for adding a user.
 */
export async function addUser(alias: string, role: Role, hash: string) {
  const id = storage.length;

  const new_user = {
    alias,
    password: hash,
    role,
    id,
  };

  userdatabas_alias_index[alias] = new_user;
  storage.push(new_user);
  await save(storage);
}

export async function infoAdmin() {
  return {
    title: "Accept user:",
    title2: "All users:",
    waiting: (await load("waiting", waiting_list)).map((i) => {
      if (i) {
        return i;
      } else {
        return null;
      }
    }).filter((n) => n),
    users: (await load("user", waiting_list)).map((i) => {
      if (i) {
        return i;
      } else {
        return null;
      }
    }).filter((n) => n),
  };
}

// Aiding functions

/**
 * Create a index for a list.
 */
export function index<T extends object>(storage: Array<T>, key: keyof T) {
  const result: { [key: string]: T } = {};
  for (const item of storage) {
    if (Object.hasOwn(item, key)) {
      const value = String(item[key]);
      result[value] = item;
    }
  }
  return result;
}

export async function hash(text: string, mode = "SHA-256"): Promise<string> {
  const messageBuffer = new TextEncoder().encode(text);
  const hashBuffer = await crypto.subtle.digest(mode, messageBuffer);
  const hash = encodeHex(hashBuffer);
  return hash;
}

// Obser this will work if you want all properties. But fail if you expetet a specific array of object.
export function getKeys<T extends object>(obj: T): (keyof T)[] {
  return Object.keys(obj) as ((keyof T)[]);
}
