import {
  CanActivate,
  createParamDecorator,
  ExecutionContext,
  Injectable,
  UnauthorizedException,
} from '@nestjs/common';
import { createHmac } from 'crypto';
import { PinoLogger } from 'nestjs-pino';

export interface TwitchJwtPayload {
  opaque_user_id: string;
  channel_id: string;
  role: string;
  exp: number;
}

interface HttpRequest {
  headers: Record<string, string | string[] | undefined>;
  twitchClaims?: TwitchJwtPayload;
  log?: { bindings: () => Record<string, unknown>; child: (b: Record<string, unknown>) => unknown };
}

export const TwitchClaims = createParamDecorator(
  (_: unknown, ctx: ExecutionContext): TwitchJwtPayload => {
    const req = ctx.switchToHttp().getRequest<HttpRequest>();
    if (!req.twitchClaims) throw new UnauthorizedException('Missing claims');
    return req.twitchClaims;
  }
);

@Injectable()
export class TwitchJwtGuard implements CanActivate {
  constructor(private readonly logger: PinoLogger) {
    this.logger.setContext(TwitchJwtGuard.name);
  }

  canActivate(ctx: ExecutionContext): boolean {
    const req = ctx.switchToHttp().getRequest<HttpRequest>();
    const authHeader = req.headers['authorization'];
    const auth = Array.isArray(authHeader) ? authHeader[0] : authHeader;
    if (!auth?.startsWith('Bearer ')) throw new UnauthorizedException();

    const token = auth.slice(7);
    const devClaims = tryDevBypass(token);
    if (devClaims) {
      req.twitchClaims = devClaims;
      this.logger.warn({ msg: 'dev auth bypass active', opaqueUserId: devClaims.opaque_user_id });
      this.logger.assign({ channelId: devClaims.channel_id, opaqueUserId: devClaims.opaque_user_id });
      return true;
    }

    req.twitchClaims = verifyAndDecode(token);

    // Bind Twitch identity into this request's pino logger so every subsequent
    // log line in the request lifecycle carries channelId and opaqueUserId.
    this.logger.assign({
      channelId: req.twitchClaims.channel_id,
      opaqueUserId: req.twitchClaims.opaque_user_id,
    });

    return true;
  }
}

/**
 * Accepts the fake dev JWT minted by TwitchAuthService.buildDevAuth() when
 * NODE_ENV=development. Tokens are identified by their `.dev` signature and
 * a UDEV-prefixed opaque_user_id. Fails loudly if called outside dev mode —
 * this guard must NEVER run in production.
 */
function tryDevBypass(token: string): TwitchJwtPayload | null {
  if (process.env['NODE_ENV'] !== 'development') return null;

  const parts = token.split('.');
  if (parts.length !== 3 || parts[2] !== 'dev') return null;

  try {
    const raw = Buffer.from(
      parts[1].replace(/-/g, '+').replace(/_/g, '/'),
      'base64'
    ).toString('utf8');
    const payload = JSON.parse(raw) as TwitchJwtPayload;
    if (!payload.opaque_user_id?.startsWith('UDEV')) return null;
    return payload;
  } catch {
    return null;
  }
}

function verifyAndDecode(token: string): TwitchJwtPayload {
  const parts = token.split('.');
  if (parts.length !== 3) throw new UnauthorizedException('Malformed JWT');

  const [header, payloadB64, sig] = parts;
  const secret = process.env['TWITCH_EXTENSION_SECRET'];
  if (!secret) throw new UnauthorizedException('No extension secret configured');

  // Twitch shared secret is base64-encoded; decode to raw bytes for HMAC.
  const secretBytes = Buffer.from(secret, 'base64');
  const expected = createHmac('sha256', secretBytes)
    .update(`${header}.${payloadB64}`)
    .digest('base64url');

  if (expected !== sig) throw new UnauthorizedException('Invalid signature');

  const raw = Buffer.from(
    payloadB64.replace(/-/g, '+').replace(/_/g, '/'),
    'base64'
  ).toString('utf8');

  const payload = JSON.parse(raw) as TwitchJwtPayload;

  if (payload.exp < Math.floor(Date.now() / 1000)) {
    throw new UnauthorizedException('Token expired');
  }

  return payload;
}
