import { createHmac, timingSafeEqual, randomBytes } from 'crypto';

export const TICKET_TTL_SECONDS = 300;
export const TICKET_NONCE_BYTES = 16;

export type QrTokenScope = 'checkpoint' | 'shared';

export interface TicketPayload {
  companyId: string;
  targetMode: 'local' | 'external';
  targetCode: string | null;
  targetUrl: string | null;
  dispatchCheckpointId: string;
  dispatchOrigin: string;
  ts: number;
  nonce: string;
}

export interface QrTokenValidationResult {
  valid: boolean;
  reason?: 'invalid' | 'expired' | 'malformed';
}

function getSecretOrThrow(): Buffer {
  const secretHex = process.env.SHARED_CHECKIN_SECRET;
  if (!secretHex) {
    throw new Error('SHARED_CHECKIN_SECRET_MISSING');
  }
  if (secretHex.length < 32) {
    throw new Error('SHARED_CHECKIN_SECRET_TOO_SHORT');
  }
  return Buffer.from(secretHex, 'hex');
}

function computeHmacHex(payload: string): string {
  return createHmac('sha256', getSecretOrThrow()).update(payload).digest('hex');
}

function safeEqualHex(a: string, b: string): boolean {
  if (a.length !== b.length) return false;
  try {
    return timingSafeEqual(Buffer.from(a, 'hex'), Buffer.from(b, 'hex'));
  } catch {
    return false;
  }
}

export function getAllowedHosts(): string[] {
  return (process.env.SHARED_CHECKIN_ALLOWED_HOSTS || '')
    .split(',')
    .map((s) => s.trim())
    .filter(Boolean);
}

export function isAllowedHost(url: string): boolean {
  const allowed = getAllowedHosts();
  if (allowed.length === 0) return false;
  try {
    const parsed = new URL(url);
    return allowed.includes(parsed.hostname);
  } catch {
    return false;
  }
}

function computeBucket(nowMs: number, rotationSeconds: number): number {
  return Math.floor(nowMs / (rotationSeconds * 1000));
}

export interface IssueQrTokenParams {
  scope: QrTokenScope;
  checkpointCode: string;
  rotationSeconds: number | null;
  now?: number;
}

export interface IssueQrTokenResult {
  token: string;
  expiresAt: number | null;
}

export function issueQrToken(params: IssueQrTokenParams): IssueQrTokenResult {
  const { scope, checkpointCode, rotationSeconds } = params;
  const now = params.now ?? Date.now();

  if (rotationSeconds == null) {
    const mac = computeHmacHex(`${scope}:${checkpointCode}:permanent`);
    return { token: `permanent.${mac}`, expiresAt: null };
  }

  const bucket = computeBucket(now, rotationSeconds);
  const mac = computeHmacHex(`${scope}:${checkpointCode}:${bucket}`);
  return {
    token: `${bucket}.${mac}`,
    expiresAt: (bucket + 1) * rotationSeconds * 1000,
  };
}

export interface ValidateQrTokenParams {
  token: string;
  scope: QrTokenScope;
  checkpointCode: string;
  rotationSeconds: number | null;
  graceSeconds: number;
  now?: number;
}

export function validateQrToken(params: ValidateQrTokenParams): QrTokenValidationResult {
  const { token, scope, checkpointCode, rotationSeconds, graceSeconds } = params;
  const now = params.now ?? Date.now();

  if (!token || typeof token !== 'string') {
    return { valid: false, reason: 'malformed' };
  }

  const parts = token.split('.');
  if (parts.length !== 2) {
    return { valid: false, reason: 'malformed' };
  }
  const [bucketOrPermanent, mac] = parts;

  if (bucketOrPermanent === 'permanent') {
    const expected = computeHmacHex(`${scope}:${checkpointCode}:permanent`);
    return safeEqualHex(mac, expected)
      ? { valid: true }
      : { valid: false, reason: 'invalid' };
  }

  const claimedBucket = parseInt(bucketOrPermanent, 10);
  if (!Number.isFinite(claimedBucket)) {
    return { valid: false, reason: 'malformed' };
  }
  if (rotationSeconds == null) {
    return { valid: false, reason: 'invalid' };
  }

  const expected = computeHmacHex(`${scope}:${checkpointCode}:${claimedBucket}`);
  if (!safeEqualHex(mac, expected)) {
    return { valid: false, reason: 'invalid' };
  }

  const currentBucket = computeBucket(now, rotationSeconds);
  if (claimedBucket === currentBucket) {
    return { valid: true };
  }
  if (claimedBucket === currentBucket - 1) {
    const intoCurrent = now - currentBucket * rotationSeconds * 1000;
    if (intoCurrent < graceSeconds * 1000) {
      return { valid: true };
    }
  }
  return { valid: false, reason: 'expired' };
}

export interface IssueTicketParams {
  companyId: string;
  targetMode: 'local' | 'external';
  targetCode: string | null;
  targetUrl: string | null;
  dispatchCheckpointId: string;
  dispatchOrigin: string;
  now?: number;
}

export interface IssueTicketResult {
  ticket: string;
  payload: TicketPayload;
  expiresAt: number;
}

export function issueTicket(params: IssueTicketParams): IssueTicketResult {
  const now = params.now ?? Date.now();
  const payload: TicketPayload = {
    companyId: params.companyId,
    targetMode: params.targetMode,
    targetCode: params.targetCode,
    targetUrl: params.targetUrl,
    dispatchCheckpointId: params.dispatchCheckpointId,
    dispatchOrigin: params.dispatchOrigin,
    ts: now,
    nonce: randomBytes(TICKET_NONCE_BYTES).toString('hex'),
  };
  const payloadBase64 = Buffer.from(JSON.stringify(payload)).toString('base64url');
  const mac = computeHmacHex(payloadBase64);
  return {
    ticket: `${payloadBase64}.${mac}`,
    payload,
    expiresAt: now + TICKET_TTL_SECONDS * 1000,
  };
}

export type ValidateTicketReason =
  | 'malformed'
  | 'invalid'
  | 'expired'
  | 'origin_not_allowed';

export interface ValidateTicketShapeResult {
  valid: boolean;
  reason?: ValidateTicketReason;
  payload?: TicketPayload;
}

export function validateTicketShape(
  ticket: string,
  now?: number,
): ValidateTicketShapeResult {
  const nowMs = now ?? Date.now();
  if (!ticket || typeof ticket !== 'string') {
    return { valid: false, reason: 'malformed' };
  }
  const parts = ticket.split('.');
  if (parts.length !== 2) {
    return { valid: false, reason: 'malformed' };
  }
  const [payloadBase64, mac] = parts;

  const expected = computeHmacHex(payloadBase64);
  if (!safeEqualHex(mac, expected)) {
    return { valid: false, reason: 'invalid' };
  }

  let payload: TicketPayload;
  try {
    payload = JSON.parse(
      Buffer.from(payloadBase64, 'base64url').toString('utf8'),
    ) as TicketPayload;
  } catch {
    return { valid: false, reason: 'malformed' };
  }

  if (
    typeof payload.ts !== 'number' ||
    typeof payload.nonce !== 'string' ||
    typeof payload.companyId !== 'string' ||
    typeof payload.dispatchCheckpointId !== 'string' ||
    typeof payload.dispatchOrigin !== 'string'
  ) {
    return { valid: false, reason: 'malformed' };
  }

  if (nowMs - payload.ts > TICKET_TTL_SECONDS * 1000) {
    return { valid: false, reason: 'expired' };
  }

  if (!isAllowedHost(payload.dispatchOrigin)) {
    return { valid: false, reason: 'origin_not_allowed' };
  }

  return { valid: true, payload };
}
