/**
 * Push Notification Service - APNS Integration
 *
 * Sends push notifications to iOS devices via Apple Push Notification Service.
 * Uses HTTP/2 directly (Node.js built-in) to avoid external dependencies.
 *
 * If APNS credentials are not configured, falls back to a stub that logs
 * notifications to console instead of sending them.
 *
 * Environment variables:
 * - APNS_KEY_ID: Key ID from Apple Developer account
 * - APNS_TEAM_ID: Team ID from Apple Developer account
 * - APNS_KEY_PATH: Path to .p8 private key file
 * - APNS_BUNDLE_ID: App bundle identifier (e.g., com.craigvg.lichunWebsocket)
 * - APNS_PRODUCTION: Set to 'true' for production APNS gateway
 */

import http2 from 'node:http2';
import fs from 'node:fs';
import crypto from 'node:crypto';

// Notification category identifiers (matched on iOS side)
export type NotificationCategory =
  | 'life_event'
  | 'relationship'
  | 'milestone'
  | 'holiday'
  | 'reminder';

/**
 * Deep link types for notification routing on the iOS side.
 * The iOS app uses these to navigate to the relevant screen on tap.
 */
export type DeepLinkType = 'event' | 'chat' | 'milestone';

export interface DeepLinkPayload {
  type: DeepLinkType;
  id: string;
}

export interface PushPayload {
  title: string;
  body: string;
  category?: NotificationCategory;
  badge?: number;
  sound?: string;
  /** Arbitrary extra data included in the APNS payload (outside `aps`) */
  data?: Record<string, unknown>;
  /** Structured deep link for iOS routing on notification tap */
  deepLink?: DeepLinkPayload;
}

export interface SendResult {
  success: boolean;
  error?: string;
  statusCode?: number;
}

export interface ApnsConfig {
  keyId: string;
  teamId: string;
  keyData: string;
  bundleId: string;
  production: boolean;
}

export interface CharacterLiveActivityContentState {
  gameDate: string;
  gameTime: string;
  age: number;
  season: string;
  location: string;
  statusMessage: string;
  energy: number;
  health: number;
  happiness: number;
  speedLabel: string;
  isRunning: boolean;
  updatedAt: string;
}

export interface LiveActivityPayload {
  event: 'update' | 'end';
  timestamp?: number;
  staleDate?: number;
  dismissalDate?: number;
  contentState: CharacterLiveActivityContentState;
}

export interface BuiltApnsRequest {
  host: string;
  path: string;
  headers: Record<string, string | number>;
  payload: Record<string, unknown>;
  body: string;
}

const APNS_HOST_DEV = 'api.sandbox.push.apple.com';
const APNS_HOST_PROD = 'api.push.apple.com';
const JWT_TOKEN_TTL = 3500; // Refresh JWT every ~58 minutes (Apple allows 60 min)

let apnsConfig: ApnsConfig | null = null;
let cachedJwt: { token: string; issuedAt: number } | null = null;

/**
 * Load APNS configuration from environment variables.
 * Returns null if any required variable is missing.
 */
function loadApnsConfig(): ApnsConfig | null {
  const keyId = process.env.APNS_KEY_ID;
  const teamId = process.env.APNS_TEAM_ID;
  const keyPath = process.env.APNS_KEY_PATH;
  const bundleId = process.env.APNS_BUNDLE_ID;
  const production = process.env.APNS_PRODUCTION === 'true';

  if (!keyId || !teamId || !keyPath || !bundleId) {
    return null;
  }

  try {
    const keyData = fs.readFileSync(keyPath, 'utf8');
    return { keyId, teamId, keyData, bundleId, production };
  } catch {
    console.warn(`[APNS] Could not read key file at ${keyPath}`);
    return null;
  }
}

/**
 * Generate a JWT token for APNS authentication.
 * Tokens are cached and reused until they expire.
 */
function getJwtToken(config: ApnsConfig): string {
  const now = Math.floor(Date.now() / 1000);

  if (cachedJwt && now - cachedJwt.issuedAt < JWT_TOKEN_TTL) {
    return cachedJwt.token;
  }

  const header = Buffer.from(
    JSON.stringify({ alg: 'ES256', kid: config.keyId })
  ).toString('base64url');

  const payload = Buffer.from(
    JSON.stringify({ iss: config.teamId, iat: now })
  ).toString('base64url');

  const sign = crypto.createSign('SHA256');
  sign.update(`${header}.${payload}`);
  const signature = sign.sign(config.keyData, 'base64url');

  const token = `${header}.${payload}.${signature}`;
  cachedJwt = { token, issuedAt: now };
  return token;
}

/**
 * Check if APNS is configured and available.
 */
export function isApnsAvailable(): boolean {
  if (!apnsConfig) {
    apnsConfig = loadApnsConfig();
  }
  return apnsConfig !== null;
}

/**
 * Send a push notification to a specific device token.
 *
 * If APNS is not configured, logs the notification to console (stub mode).
 */
export async function sendPushNotification(
  deviceToken: string,
  payload: PushPayload
): Promise<SendResult> {
  if (!deviceToken) {
    return { success: false, error: 'No device token provided' };
  }

  // Load config on first call
  if (!apnsConfig) {
    apnsConfig = loadApnsConfig();
  }

  // Stub mode: log instead of sending
  if (!apnsConfig) {
    console.log(
      `[APNS Stub] Would send to ${deviceToken.slice(0, 8)}...: "${payload.title}" - "${payload.body}" (category: ${payload.category ?? 'none'})`
    );
    return { success: true };
  }

  return sendViaHttp2(deviceToken, payload, apnsConfig);
}

/**
 * Send an ActivityKit Live Activity update to a specific activity push token.
 */
export async function sendLiveActivityUpdate(
  activityPushToken: string,
  payload: LiveActivityPayload
): Promise<SendResult> {
  if (!activityPushToken) {
    return { success: false, error: 'No live activity token provided' };
  }

  if (!apnsConfig) {
    apnsConfig = loadApnsConfig();
  }

  if (!apnsConfig) {
    console.log(
      `[APNS Stub] Would send live activity ${payload.event} to ${activityPushToken.slice(0, 8)}...`
    );
    return { success: true };
  }

  return sendLiveActivityViaHttp2(activityPushToken, payload, apnsConfig);
}

export function buildAlertApnsRequest(
  deviceToken: string,
  payload: PushPayload,
  config: ApnsConfig,
  jwt: string
): BuiltApnsRequest {
  const host = config.production ? APNS_HOST_PROD : APNS_HOST_DEV;

  const aps: Record<string, unknown> = {
    alert: {
      title: payload.title,
      body: payload.body,
    },
    sound: payload.sound ?? 'default',
    'mutable-content': 1,
    category: payload.category ?? 'life_event',
  };
  if (payload.badge !== undefined) {
    aps.badge = payload.badge;
  }

  const apnsPayload: Record<string, unknown> = {
    aps,
    ...payload.data,
  };

  if (payload.deepLink) {
    apnsPayload.deepLink = payload.deepLink;
  }

  const body = JSON.stringify(apnsPayload);

  return {
    host,
    path: `/3/device/${deviceToken}`,
    headers: {
      ':method': 'POST',
      ':path': `/3/device/${deviceToken}`,
      authorization: `bearer ${jwt}`,
      'apns-topic': config.bundleId,
      'apns-push-type': 'alert',
      'apns-priority': '10',
      'content-type': 'application/json',
      'content-length': Buffer.byteLength(body),
    },
    payload: apnsPayload,
    body,
  };
}

export function buildLiveActivityApnsRequest(
  activityPushToken: string,
  payload: LiveActivityPayload,
  config: ApnsConfig,
  jwt: string
): BuiltApnsRequest {
  const host = config.production ? APNS_HOST_PROD : APNS_HOST_DEV;
  const timestamp = payload.timestamp ?? Math.floor(Date.now() / 1000);

  const aps: Record<string, unknown> = {
    event: payload.event,
    timestamp,
    'content-state': payload.contentState,
  };

  if (payload.staleDate !== undefined) {
    aps['stale-date'] = payload.staleDate;
  }
  if (payload.dismissalDate !== undefined) {
    aps['dismissal-date'] = payload.dismissalDate;
  }

  const apnsPayload: Record<string, unknown> = { aps };
  const body = JSON.stringify(apnsPayload);

  return {
    host,
    path: `/3/device/${activityPushToken}`,
    headers: {
      ':method': 'POST',
      ':path': `/3/device/${activityPushToken}`,
      authorization: `bearer ${jwt}`,
      'apns-topic': `${config.bundleId}.push-type.liveactivity`,
      'apns-push-type': 'liveactivity',
      'apns-priority': '10',
      'content-type': 'application/json',
      'content-length': Buffer.byteLength(body),
    },
    payload: apnsPayload,
    body,
  };
}

/**
 * Send notification via HTTP/2 to APNS gateway.
 */
function sendViaHttp2(
  deviceToken: string,
  payload: PushPayload,
  config: ApnsConfig
): Promise<SendResult> {
  return new Promise((resolve) => {
    const jwt = getJwtToken(config);
    const builtRequest = buildAlertApnsRequest(deviceToken, payload, config, jwt);

    let client: http2.ClientHttp2Session;
    try {
      client = http2.connect(`https://${builtRequest.host}`);
    } catch (err) {
      resolve({
        success: false,
        error: `HTTP/2 connect failed: ${err instanceof Error ? err.message : String(err)}`,
      });
      return;
    }

    client.on('error', (err) => {
      resolve({ success: false, error: `HTTP/2 error: ${err.message}` });
      client.close();
    });

    const req = client.request(builtRequest.headers);

    let responseData = '';
    let statusCode = 0;

    req.on('response', (headers) => {
      statusCode = headers[':status'] as number;
    });

    req.on('data', (chunk: Buffer) => {
      responseData += chunk.toString();
    });

    req.on('end', () => {
      client.close();

      if (statusCode === 200) {
        resolve({ success: true, statusCode });
      } else {
        let error = `APNS returned ${statusCode}`;
        try {
          const parsed = JSON.parse(responseData);
          error = parsed.reason ?? error;
        } catch {
          // Use raw status code error
        }
        console.warn(`[APNS] Send failed for ${deviceToken.slice(0, 8)}...: ${error}`);
        resolve({ success: false, error, statusCode });
      }
    });

    req.on('error', (err) => {
      client.close();
      resolve({ success: false, error: `Request error: ${err.message}` });
    });

    req.write(builtRequest.body);
    req.end();
  });
}

function sendLiveActivityViaHttp2(
  activityPushToken: string,
  payload: LiveActivityPayload,
  config: ApnsConfig
): Promise<SendResult> {
  return new Promise((resolve) => {
    const jwt = getJwtToken(config);
    const builtRequest = buildLiveActivityApnsRequest(activityPushToken, payload, config, jwt);

    let client: http2.ClientHttp2Session;
    try {
      client = http2.connect(`https://${builtRequest.host}`);
    } catch (err) {
      resolve({
        success: false,
        error: `HTTP/2 connect failed: ${err instanceof Error ? err.message : String(err)}`,
      });
      return;
    }

    client.on('error', (err) => {
      resolve({ success: false, error: `HTTP/2 error: ${err.message}` });
      client.close();
    });

    const req = client.request(builtRequest.headers);

    let responseData = '';
    let statusCode = 0;

    req.on('response', (headers) => {
      statusCode = headers[':status'] as number;
    });

    req.on('data', (chunk: Buffer) => {
      responseData += chunk.toString();
    });

    req.on('end', () => {
      client.close();

      if (statusCode === 200) {
        resolve({ success: true, statusCode });
      } else {
        let error = `APNS returned ${statusCode}`;
        try {
          const parsed = JSON.parse(responseData);
          error = parsed.reason ?? error;
        } catch {
          // Use raw status code error
        }
        console.warn(`[APNS] Live Activity send failed for ${activityPushToken.slice(0, 8)}...: ${error}`);
        resolve({ success: false, error, statusCode });
      }
    });

    req.on('error', (err) => {
      client.close();
      resolve({ success: false, error: `Request error: ${err.message}` });
    });

    req.write(builtRequest.body);
    req.end();
  });
}
