import { Application, path, Router } from "./deps.ts";
import { Manifest } from "../common/manifest.ts";
import { SpacePrimitives } from "../common/spaces/space_primitives.ts";
import { EndpointHook } from "../plugos/hooks/endpoint.ts";
import { AssetBundle } from "../plugos/asset_bundle/bundle.ts";
import { SpaceSystem } from "./space_system.ts";
import { ensureAndLoadSettings } from "../common/util.ts";
import { base64Decode } from "../plugos/asset_bundle/base64.ts";

export type ServerOptions = {
  hostname: string;
  port: number;
  pagesPath: string;
  dbPath: string;
  assetBundle: AssetBundle;
  user?: string;
  pass?: string;
  bareMode?: boolean;
};

const staticLastModified = new Date().toUTCString();

export class HttpServer {
  app: Application;
  systemBoot: SpaceSystem;
  private hostname: string;
  private port: number;
  user?: string;
  settings: { [key: string]: any } = {};
  abortController?: AbortController;
  bareMode: boolean;

  constructor(options: ServerOptions) {
    this.hostname = options.hostname;
    this.port = options.port;
    this.app = new Application(); //{ serverConstructor: FlashServer });
    this.user = options.user;
    this.systemBoot = new SpaceSystem(
      options.assetBundle,
      options.pagesPath,
      options.dbPath,
    );
    this.bareMode = options.bareMode || false;

    // Second, for loading plug JSON files with absolute or relative (from CWD) paths
    this.systemBoot.eventHook.addLocalListener(
      "get-plug:file",
      async (plugPath: string): Promise<Manifest> => {
        const resolvedPath = path.resolve(plugPath);
        try {
          const manifestJson = await Deno.readTextFile(resolvedPath);
          return JSON.parse(manifestJson);
        } catch {
          throw new Error(
            `No such file: ${resolvedPath} or could not parse as JSON`,
          );
        }
      },
    );

    // Rescan disk every 5s to detect any out-of-process file changes
    setInterval(() => {
      this.systemBoot.space.updatePageList().catch(console.error);
    }, 5000);

    // Register the HTTP endpoint hook (with "/_/<plug-name>"" prefix, hardcoded for now)
    this.systemBoot.system.addHook(new EndpointHook(this.app, "/_"));
  }

  async start() {
    await this.systemBoot.start();
    await this.systemBoot.ensureSpaceIndex();
    await ensureAndLoadSettings(this.systemBoot.space, this.bareMode);

    this.addPasswordAuth(this.app);

    // Serve static files (javascript, css, html)
    this.app.use(async ({ request, response }, next) => {
      if (request.url.pathname === "/") {
        if (request.headers.get("If-Modified-Since") === staticLastModified) {
          response.status = 304;
          return;
        }
        response.headers.set("Content-type", "text/html");
        response.body = this.systemBoot.assetBundle.readTextFileSync(
          "web/index.html",
        );
        response.headers.set("Last-Modified", staticLastModified);
        return;
      }
      try {
        const assetName = `web${request.url.pathname}`;
        if (
          this.systemBoot.assetBundle.has(assetName) &&
          request.headers.get("If-Modified-Since") === staticLastModified
        ) {
          response.status = 304;
          return;
        }
        response.status = 200;
        response.headers.set(
          "Content-type",
          this.systemBoot.assetBundle.getMimeType(assetName),
        );
        const data = this.systemBoot.assetBundle.readFileSync(
          assetName,
        );
        response.headers.set("Cache-Control", "no-cache");
        response.headers.set("Content-length", "" + data.length);
        response.headers.set("Last-Modified", staticLastModified);

        if (request.method === "GET") {
          response.body = data;
        }
      } catch {
        await next();
      }
    });

    // Pages API
    const fsRouter = this.buildFsRouter(this.systemBoot.spacePrimitives);
    this.app.use(fsRouter.routes());
    this.app.use(fsRouter.allowedMethods());

    // Plug API
    const plugRouter = this.buildPlugRouter();
    this.app.use(plugRouter.routes());
    this.app.use(plugRouter.allowedMethods());

    // Fallback, serve index.html
    this.app.use((ctx) => {
      ctx.response.headers.set("Content-type", "text/html");
      ctx.response.body = this.systemBoot.assetBundle.readTextFileSync(
        "web/index.html",
      );
    });

    this.abortController = new AbortController();
    this.app.listen({
      hostname: this.hostname,
      port: this.port,
      signal: this.abortController.signal,
    })
      .catch((e: any) => {
        console.log("Server listen error:", e.message);
        Deno.exit(1);
      });
    const visibleHostname = this.hostname === "0.0.0.0"
      ? "localhost"
      : this.hostname;
    console.log(
      `SilverBullet is now running: http://${visibleHostname}:${this.port}`,
    );
  }

  private addPasswordAuth(app: Application) {
    const excludedPaths = [
      "/manifest.json",
      "/favicon.png",
      "/logo.png",
      "/.auth",
    ];
    if (this.user) {
      const b64User = btoa(this.user);
      app.use(async ({ request, response, cookies }, next) => {
        if (!excludedPaths.includes(request.url.pathname)) {
          const authCookie = await cookies.get("auth");
          if (!authCookie || authCookie !== b64User) {
            response.redirect(`/.auth?refer=${request.url.pathname}`);
            return;
          }
        }
        if (request.url.pathname === "/.auth") {
          if (request.method === "GET") {
            response.headers.set("Content-type", "text/html");
            response.body = this.systemBoot.assetBundle.readTextFileSync(
              "web/auth.html",
            );
            return;
          } else if (request.method === "POST") {
            const values = await request.body({ type: "form" }).value;
            const username = values.get("username"),
              password = values.get("password"),
              refer = values.get("refer");
            if (this.user === `${username}:${password}`) {
              await cookies.set("auth", b64User, {
                expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7), // in a week
                sameSite: "strict",
              });
              response.redirect(refer || "/");
              // console.log("All headers", request.headers);
            } else {
              response.redirect("/.auth?error=1");
            }
            return;
          } else {
            response.status = 401;
            response.body = "Unauthorized";
            return;
          }
        } else {
          // Unauthenticated access to excluded paths
          await next();
        }
      });
    }
  }

  private buildFsRouter(spacePrimitives: SpacePrimitives): Router {
    const fsRouter = new Router();
    // File list
    fsRouter.get("/", async ({ response }) => {
      response.headers.set("Content-type", "application/json");
      const files = await spacePrimitives.fetchFileList();
      response.body = JSON.stringify(files);
    });

    fsRouter
      .get("\/(.+)", async ({ params, response, request }) => {
        const name = params[0];
        // console.log("Loading file", name);
        try {
          const attachmentData = await spacePrimitives.readFile(
            name,
            "arraybuffer",
          );
          const lastModifiedHeader = new Date(attachmentData.meta.lastModified)
            .toUTCString();
          if (request.headers.get("If-Modified-Since") === lastModifiedHeader) {
            response.status = 304;
            return;
          }
          response.status = 200;
          response.headers.set(
            "X-Last-Modified",
            "" + attachmentData.meta.lastModified,
          );
          response.headers.set("Cache-Control", "no-cache");
          response.headers.set("X-Permission", attachmentData.meta.perm);
          response.headers.set(
            "Last-Modified",
            lastModifiedHeader,
          );
          response.headers.set("Content-Type", attachmentData.meta.contentType);
          response.body = attachmentData.data as ArrayBuffer;
        } catch {
          // console.error("Error in main router", e);
          response.status = 404;
          response.body = "";
        }
      })
      .put("\/(.+)", async ({ request, response, params }) => {
        const name = params[0];
        console.log("Saving file", name);

        let body: Uint8Array;
        if (
          request.headers.get("X-Content-Base64")
        ) {
          const content = await request.body({ type: "text" }).value;
          body = base64Decode(content);
        } else {
          body = await request.body({ type: "bytes" }).value;
        }

        try {
          const meta = await spacePrimitives.writeFile(
            name,
            "arraybuffer",
            body,
          );
          response.status = 200;
          response.headers.set("Content-Type", meta.contentType);
          response.headers.set("X-Last-Modified", "" + meta.lastModified);
          response.headers.set("X-Content-Length", "" + meta.size);
          response.headers.set("X-Permission", meta.perm);
          response.body = "OK";
        } catch (err) {
          response.status = 500;
          response.body = "Write failed";
          console.error("Pipeline failed", err);
        }
      })
      .options("\/(.+)", async ({ response, params }) => {
        const name = params[0];
        try {
          const meta = await spacePrimitives.getFileMeta(name);
          response.status = 200;
          response.headers.set("Content-Type", meta.contentType);
          response.headers.set("X-Last-Modified", "" + meta.lastModified);
          response.headers.set("X-Content-Length", "" + meta.size);
          response.headers.set("X-Permission", meta.perm);
        } catch {
          response.status = 404;
          response.body = "File not found";
          // console.error("Options failed", err);
        }
      })
      .delete("\/(.+)", async ({ response, params }) => {
        const name = params[0];
        try {
          await spacePrimitives.deleteFile(name);
          response.status = 200;
          response.body = "OK";
        } catch (e: any) {
          console.error("Error deleting attachment", e);
          response.status = 200;
          response.body = e.message;
        }
      });
    return new Router().use("/fs", fsRouter.routes());
  }

  private buildPlugRouter(): Router {
    const plugRouter = new Router();
    const system = this.systemBoot.system;

    plugRouter.post(
      "/:plug/syscall/:name",
      async (ctx) => {
        const name = ctx.params.name;
        const plugName = ctx.params.plug;
        const args = await ctx.request.body().value;
        const plug = system.loadedPlugs.get(plugName);
        if (!plug) {
          ctx.response.status = 404;
          ctx.response.body = `Plug ${plugName} not found`;
          return;
        }
        try {
          const result = await system.syscallWithContext(
            { plug },
            name,
            args,
          );
          ctx.response.headers.set("Content-Type", "application/json");
          ctx.response.body = JSON.stringify(result);
        } catch (e: any) {
          console.log("Error", e);
          ctx.response.status = 500;
          ctx.response.body = e.message;
          return;
        }
      },
    );

    plugRouter.post(
      "/:plug/function/:name",
      async (ctx) => {
        const name = ctx.params.name;
        const plugName = ctx.params.plug;
        const args = await ctx.request.body().value;
        const plug = system.loadedPlugs.get(plugName);
        if (!plug) {
          ctx.response.status = 404;
          ctx.response.body = `Plug ${plugName} not found`;
          return;
        }
        try {
          const result = await plug.invoke(name, args);
          ctx.response.headers.set("Content-Type", "application/json");
          ctx.response.body = JSON.stringify(result);
        } catch (e: any) {
          ctx.response.status = 500;
          // console.log("Error invoking function", e);
          ctx.response.body = e.message;
        }
      },
    );

    return new Router().use("/plug", plugRouter.routes());
  }

  async stop() {
    const system = this.systemBoot.system;
    if (this.abortController) {
      console.log("Stopping");
      await system.unloadAll();
      console.log("Stopped plugs");
      this.abortController.abort();
      console.log("stopped server");
    }
  }
}