import { createHash, randomBytes, randomUUID } from "node:crypto";
import { verify } from "@node-rs/argon2";
import { unsafeOpaqueId, type Principal, type PrincipalId, type SessionId } from "@awp/contracts";
import type { SessionRecord, UnitOfWork } from "@awp/application";

const ABSOLUTE_TTL_MS = 30 * 24 * 60 * 60 * 1000;
const IDLE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
const LAST_SEEN_REFRESH_MS = 5 * 60 * 1000;

export interface OperatorSessionManagerOptions {
  readonly passwordHash: string;
  readonly principalId?: PrincipalId;
  readonly now?: () => Date;
  readonly verifyPassword?: (hashed: string, password: string) => Promise<boolean>;
}

export interface CreatedOperatorSession {
  readonly token: string;
  readonly session: SessionRecord;
}

export class OperatorSessionManager {
  private readonly principal: Principal;
  private readonly now: () => Date;
  private readonly credentialVersionDigest: string;
  private readonly verifyPassword: (hashed: string, password: string) => Promise<boolean>;

  constructor(
    private readonly uow: UnitOfWork,
    private readonly options: OperatorSessionManagerOptions,
  ) {
    if (!options.passwordHash.startsWith("$argon2id$")) {
      throw new Error("AWP_OPERATOR_PASSWORD_HASH must be an Argon2id encoded hash");
    }
    this.principal = {
      id: options.principalId ?? unsafeOpaqueId<PrincipalId>("principal:owner"),
      kind: "human",
      capabilities: [],
    };
    this.now = options.now ?? (() => new Date());
    this.credentialVersionDigest = tokenDigest(options.passwordHash);
    this.verifyPassword = options.verifyPassword ?? verify;
  }

  async login(password: string, userAgent?: string): Promise<CreatedOperatorSession | undefined> {
    if (!password || !(await this.verifyPassword(this.options.passwordHash, password)))
      return undefined;
    const now = this.now();
    const token = randomBytes(32).toString("base64url");
    const session: SessionRecord = {
      id: unsafeOpaqueId<SessionId>(`session:${randomUUID()}`),
      tokenHash: tokenDigest(token),
      principalId: this.principal.id,
      credentialVersionDigest: this.credentialVersionDigest,
      createdAt: now.toISOString(),
      lastSeenAt: now.toISOString(),
      expiresAt: new Date(now.getTime() + ABSOLUTE_TTL_MS).toISOString(),
      ...(userAgent ? { userAgentDigest: userAgentDigest(userAgent) } : {}),
    };
    await this.uow.transaction((tx) => tx.sessions.insert(session));
    return { token, session };
  }

  async resolve(token: string): Promise<Principal | undefined> {
    if (!token) return undefined;
    const now = this.now();
    const session = await this.uow.transaction((tx) =>
      tx.sessions.getByTokenHash(tokenDigest(token)),
    );
    if (!session || session.principalId !== this.principal.id || session.revokedAt)
      return undefined;
    if (session.credentialVersionDigest !== this.credentialVersionDigest) {
      await this.uow.transaction((tx) => tx.sessions.revoke(session.id, now.toISOString()));
      return undefined;
    }
    if (Date.parse(session.expiresAt) <= now.getTime()) return undefined;
    if (now.getTime() - Date.parse(session.lastSeenAt) > IDLE_TTL_MS) return undefined;
    if (now.getTime() - Date.parse(session.lastSeenAt) >= LAST_SEEN_REFRESH_MS) {
      await this.uow.transaction((tx) => tx.sessions.updateLastSeen(session.id, now.toISOString()));
    }
    return this.principal;
  }

  async revoke(token: string): Promise<boolean> {
    if (!token) return false;
    const session = await this.uow.transaction((tx) =>
      tx.sessions.getByTokenHash(tokenDigest(token)),
    );
    if (!session || session.revokedAt) return false;
    await this.uow.transaction((tx) => tx.sessions.revoke(session.id, this.now().toISOString()));
    return true;
  }

  async revokeAll(): Promise<number> {
    return this.uow.transaction((tx) =>
      tx.sessions.revokeAllByPrincipal(this.principal.id, this.now().toISOString()),
    );
  }
}

export function tokenDigest(token: string): string {
  return createHash("sha256").update(token, "utf8").digest("hex");
}

export function userAgentDigest(userAgent: string): string {
  return createHash("sha256").update(userAgent, "utf8").digest("hex");
}
