export const ADMIN_APP_SESSION_COOKIE = "xtablo-admin-app-session";
const ADMIN_ACCESS_PATH = "/__admin/access";
const ADMIN_LOGOUT_PATH = "/__admin/logout";
const SESSION_TTL_SECONDS = 60 * 60 * 12;

type WorkerEnv = {
  ADMIN_APP_ACCESS_TOKEN: string;
  ADMIN_APP_SESSION_SECRET: string;
  ASSETS: {
    fetch: (request: Request) => Promise<Response>;
  };
};

function base64UrlEncode(bytes: Uint8Array) {
  return btoa(String.fromCharCode(...bytes))
    .replace(/\+/g, "-")
    .replace(/\//g, "_")
    .replace(/=/g, "");
}

function base64UrlDecode(value: string) {
  const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
  const padding = normalized.length % 4 === 0 ? "" : "=".repeat(4 - (normalized.length % 4));
  const binary = atob(`${normalized}${padding}`);

  return Uint8Array.from(binary, (character) => character.charCodeAt(0));
}

async function importSigningKey(secret: string) {
  return crypto.subtle.importKey(
    "raw",
    new TextEncoder().encode(secret),
    { name: "HMAC", hash: "SHA-256" },
    false,
    ["sign", "verify"]
  );
}

async function signValue(value: string, secret: string) {
  const key = await importSigningKey(secret);
  const signature = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(value));

  return base64UrlEncode(new Uint8Array(signature));
}

async function verifyValueSignature(value: string, signature: string, secret: string) {
  const key = await importSigningKey(secret);

  return crypto.subtle.verify(
    "HMAC",
    key,
    base64UrlDecode(signature),
    new TextEncoder().encode(value)
  );
}

function parseCookie(cookieHeader: string | null, cookieName: string) {
  if (!cookieHeader) {
    return null;
  }

  const match = cookieHeader.match(new RegExp(`(?:^|;\\s*)${cookieName}=([^;]+)`));
  return match?.[1] ?? null;
}

export async function createSignedAdminAppSession(secret: string, now = Date.now()) {
  const expiresAt = Math.floor(now / 1000) + SESSION_TTL_SECONDS;
  const payload = `${expiresAt}`;
  const signature = await signValue(payload, secret);

  return `${payload}.${signature}`;
}

export async function hasValidAdminAppSession(request: Request, secret: string, now = Date.now()) {
  const sessionCookie = parseCookie(request.headers.get("cookie"), ADMIN_APP_SESSION_COOKIE);
  if (!sessionCookie) {
    return false;
  }

  const [expiresAtValue, signature] = sessionCookie.split(".");
  if (!expiresAtValue || !signature) {
    return false;
  }

  const expiresAt = Number.parseInt(expiresAtValue, 10);
  if (Number.isNaN(expiresAt) || expiresAt <= Math.floor(now / 1000)) {
    return false;
  }

  return verifyValueSignature(expiresAtValue, signature, secret);
}

function isHtmlRequest(request: Request) {
  const accept = request.headers.get("accept") ?? "";
  return accept.includes("text/html") || accept.includes("*/*");
}

function buildSessionCookie(session: string) {
  return `${ADMIN_APP_SESSION_COOKIE}=${session}; HttpOnly; Path=/; SameSite=Strict; Secure; Max-Age=${SESSION_TTL_SECONDS}`;
}

function buildExpiredSessionCookie() {
  return `${ADMIN_APP_SESSION_COOKIE}=; HttpOnly; Path=/; SameSite=Strict; Secure; Max-Age=0`;
}

export function buildAccessDeniedHtml(error?: string) {
  return `<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>XTablo Admin Access</title>
    <style>
      :root {
        color-scheme: light;
        font-family: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
        background: #f7f3ea;
        color: #162033;
      }
      body {
        margin: 0;
        min-height: 100vh;
        display: grid;
        place-items: center;
        background:
          radial-gradient(circle at top left, rgba(15, 118, 110, 0.15), transparent 32%),
          linear-gradient(135deg, #f9f4ea 0%, #f1ede3 48%, #efe4d1 100%);
      }
      main {
        width: min(92vw, 440px);
        background: rgba(255, 252, 247, 0.92);
        border: 1px solid rgba(22, 32, 51, 0.12);
        border-radius: 28px;
        padding: 32px;
        box-shadow: 0 24px 80px rgba(22, 32, 51, 0.12);
      }
      .eyebrow {
        font: 600 11px/1.2 ui-sans-serif, system-ui, sans-serif;
        letter-spacing: 0.22em;
        text-transform: uppercase;
        color: #6a7280;
      }
      h1 {
        margin: 14px 0 0;
        font-size: 2rem;
        line-height: 1.05;
      }
      p {
        margin: 14px 0 0;
        font: 400 0.98rem/1.55 ui-sans-serif, system-ui, sans-serif;
        color: #465062;
      }
      form {
        margin-top: 24px;
      }
      label {
        display: block;
        font: 600 0.88rem/1.4 ui-sans-serif, system-ui, sans-serif;
      }
      input {
        width: 100%;
        margin-top: 10px;
        padding: 14px 16px;
        border-radius: 18px;
        border: 1px solid rgba(22, 32, 51, 0.16);
        background: white;
        font: 400 0.95rem/1.4 ui-monospace, SFMono-Regular, monospace;
        box-sizing: border-box;
      }
      button {
        width: 100%;
        margin-top: 16px;
        border: 0;
        border-radius: 18px;
        padding: 14px 16px;
        background: #172554;
        color: white;
        font: 600 0.95rem/1.2 ui-sans-serif, system-ui, sans-serif;
        cursor: pointer;
      }
      .error {
        margin-top: 16px;
        padding: 12px 14px;
        border-radius: 16px;
        background: #fef2f2;
        color: #b91c1c;
        font: 600 0.85rem/1.4 ui-sans-serif, system-ui, sans-serif;
      }
      .footnote {
        margin-top: 18px;
        font-size: 0.82rem;
      }
    </style>
  </head>
  <body>
    <main>
      <div class="eyebrow">Internal Only</div>
      <h1>Internal Admin Access</h1>
      <p>
        This app is firewalled behind a dedicated app-access token before any admin session
        can be established.
      </p>
      <form action="${ADMIN_ACCESS_PATH}" method="post">
        <label for="accessToken">App Access Token</label>
        <input id="accessToken" name="accessToken" type="password" autocomplete="off" required />
        <button type="submit">Enter Admin App</button>
      </form>
      ${error ? `<div class="error">${error}</div>` : ""}
      <p class="footnote">A second privileged token is still required inside the admin shell.</p>
    </main>
  </body>
</html>`;
}

function respondUnauthorized(request: Request, error?: string) {
  if (isHtmlRequest(request)) {
    return new Response(buildAccessDeniedHtml(error), {
      headers: {
        "Content-Type": "text/html; charset=utf-8",
        "Cache-Control": "no-store",
      },
      status: 401,
    });
  }

  return new Response("Unauthorized", { status: 401 });
}

async function handleAccessRequest(request: Request, env: WorkerEnv) {
  const formData = await request.formData().catch(() => null);
  const accessToken = formData?.get("accessToken");

  if (accessToken !== env.ADMIN_APP_ACCESS_TOKEN) {
    return respondUnauthorized(request, "Invalid app access token");
  }

  const session = await createSignedAdminAppSession(env.ADMIN_APP_SESSION_SECRET);
  const location = new URL("/", request.url).toString();

  return new Response(null, {
    headers: {
      Location: location,
      "Set-Cookie": buildSessionCookie(session),
      "Cache-Control": "no-store",
    },
    status: 302,
  });
}

async function handleLogoutRequest(request: Request) {
  return new Response(null, {
    headers: {
      Location: new URL("/", request.url).toString(),
      "Set-Cookie": buildExpiredSessionCookie(),
      "Cache-Control": "no-store",
    },
    status: 302,
  });
}

export default {
  async fetch(request: Request, env: WorkerEnv) {
    const url = new URL(request.url);

    if (request.method === "POST" && url.pathname === ADMIN_ACCESS_PATH) {
      return handleAccessRequest(request, env);
    }

    if (request.method === "POST" && url.pathname === ADMIN_LOGOUT_PATH) {
      return handleLogoutRequest(request);
    }

    const hasSession = await hasValidAdminAppSession(request, env.ADMIN_APP_SESSION_SECRET);
    if (!hasSession) {
      return respondUnauthorized(request);
    }

    return env.ASSETS.fetch(request);
  },
};
