import { createHmac, timingSafeEqual } from "node:crypto";
import { readFile, unlink, writeFile } from "fs/promises";
import path from "path";

import { DEVICE_URL_JWT_SECRET } from "@/lib/device-url-auth";

export const RUN_CONTEXT_FILE = path.join(process.cwd(), "device-url-context.json");
const RUN_CONTEXT_TOKEN_TTL_SECONDS = 30 * 60;

export type RunContext = {
  imei: string;
  partner_name: string;
  redirect_url?: string;
  savedAt?: string;
  [key: string]: unknown;
};

type RunContextStore = {
  links: Record<string, RunContext>;
};

type RunContextTokenPayload = RunContext & {
  exp: number;
};

function getContextKey(partnerName: string, imei: string) {
  return `${partnerName.trim().toLowerCase()}:${imei.trim()}`;
}

function isRunContext(value: unknown): value is RunContext {
  return Boolean(
    value &&
      typeof value === "object" &&
      typeof (value as RunContext).imei === "string" &&
      typeof (value as RunContext).partner_name === "string",
  );
}

function normalizeStore(value: unknown): RunContextStore {
  if (
    value &&
    typeof value === "object" &&
    "links" in value &&
    typeof (value as RunContextStore).links === "object" &&
    (value as RunContextStore).links
  ) {
    return (value as RunContextStore);
  }

  if (isRunContext(value)) {
    return {
      links: {
        [getContextKey(value.partner_name, value.imei)]: value,
      },
    };
  }

  return { links: {} };
}

async function readRunContextStore(): Promise<RunContextStore> {
  const file = await readFile(RUN_CONTEXT_FILE, "utf8");
  return normalizeStore(JSON.parse(file));
}

async function writeRunContextStore(store: RunContextStore) {
  await writeFile(RUN_CONTEXT_FILE, JSON.stringify(store, null, 2), "utf8");
}

function base64urlJson(value: object) {
  return Buffer.from(JSON.stringify(value)).toString("base64url");
}

function signTokenPayload(encodedPayload: string) {
  return createHmac("sha256", DEVICE_URL_JWT_SECRET)
    .update(encodedPayload)
    .digest("base64url");
}

export function createRunContextToken(context: RunContext) {
  const now = Math.floor(Date.now() / 1000);
  const encodedPayload = base64urlJson({
    ...context,
    exp: now + RUN_CONTEXT_TOKEN_TTL_SECONDS,
  });
  const signature = signTokenPayload(encodedPayload);

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

export function verifyRunContextToken(token: string): RunContext {
  const [encodedPayload, signature, extra] = token.split(".");

  if (!encodedPayload || !signature || extra !== undefined) {
    throw new Error("Malformed run context token");
  }

  const expectedSignature = Buffer.from(signTokenPayload(encodedPayload));
  const receivedSignature = Buffer.from(signature);

  if (
    receivedSignature.length !== expectedSignature.length ||
    !timingSafeEqual(receivedSignature, expectedSignature)
  ) {
    throw new Error("Invalid run context token");
  }

  const payload = JSON.parse(
    Buffer.from(encodedPayload, "base64url").toString("utf8"),
  ) as RunContextTokenPayload;
  const now = Math.floor(Date.now() / 1000);

  if (!Number.isInteger(payload.exp) || payload.exp <= now) {
    throw new Error("Expired run context token");
  }

  if (!isRunContext(payload)) {
    throw new Error("Invalid run context token payload");
  }

  const context = { ...payload };
  delete (context as Partial<RunContextTokenPayload>).exp;

  return context;
}

export async function saveRunContext(context: RunContext): Promise<boolean> {
  let store: RunContextStore;

  try {
    store = await readRunContextStore();
  } catch {
    store = { links: {} };
  }

  const key = getContextKey(context.partner_name, context.imei);

  store.links[key] = context;

  try {
    await writeRunContextStore(store);
    return true;
  } catch (error) {
    console.warn(
      "Run context file storage is unavailable; using signed token fallback:",
      error,
    );
    return false;
  }
}

export async function readRunContext(identifier?: {
  partner_name?: string;
  imei?: string;
}): Promise<RunContext> {
  const store = await readRunContextStore();

  if (identifier?.partner_name && identifier?.imei) {
    const context = store.links[getContextKey(identifier.partner_name, identifier.imei)];

    if (!context) {
      throw new Error("Run context not found");
    }

    return context;
  }

  const contexts = Object.values(store.links);

  if (contexts.length === 0) {
    throw new Error("Run context not found");
  }

  return contexts[contexts.length - 1];
}

export async function tryReadRunContext(identifier?: {
  partner_name?: string;
  imei?: string;
}): Promise<RunContext | null> {
  try {
    return await readRunContext(identifier);
  } catch {
    return null;
  }
}

export async function deleteRunContext(identifier?: {
  partner_name?: string;
  imei?: string;
}): Promise<boolean> {
  if (identifier?.partner_name && identifier?.imei) {
    try {
      let store: RunContextStore;

      try {
        store = await readRunContextStore();
      } catch {
        store = { links: {} };
      }

      const key = getContextKey(identifier.partner_name, identifier.imei);
      delete store.links[key];
      await writeRunContextStore(store);
      return true;
    } catch {
      return true;
    }
  }

  try {
    await unlink(RUN_CONTEXT_FILE);
    return true;
  } catch {
    return false;
  }
}
