import fs from 'fs';
import http2 from 'http2';
import path from 'path';
import {
  createPrivateKey,
  createSign,
  sign
} from 'crypto';
import { getDataSource } from '../db';
import { DeviceTokenEntity } from '../entities/DeviceTokenEntity';

export interface PushNotificationPayload {
  title: string;
  body: string;
  data?: Record<string, string>;
}

interface FcmServiceAccount {
  project_id: string;
  client_email: string;
  private_key: string;
}

let cachedFcmAccessToken: { token: string; expiresAt: number } | null = null;

function readJsonFile(filePath: string): unknown {
  const resolved = path.resolve(filePath);
  const raw = fs.readFileSync(resolved, 'utf8');

  return JSON.parse(raw);
}

function resolveFcmServiceAccount(): FcmServiceAccount | null {
  const inlineJson = process.env.FCM_SERVICE_ACCOUNT_JSON?.trim();

  if (inlineJson) {
    return JSON.parse(inlineJson) as FcmServiceAccount;
  }

  const filePath = process.env.FCM_SERVICE_ACCOUNT_PATH?.trim();

  if (filePath) {
    return readJsonFile(filePath) as FcmServiceAccount;
  }

  return null;
}

function base64UrlEncode(value: string | Buffer): string {
  return Buffer.from(value)
    .toString('base64')
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=+$/g, '');
}

async function getFcmAccessToken(serviceAccount: FcmServiceAccount): Promise<string> {
  if (cachedFcmAccessToken && cachedFcmAccessToken.expiresAt > Date.now() + 60_000) {
    return cachedFcmAccessToken.token;
  }

  const now = Math.floor(Date.now() / 1000);
  const header = base64UrlEncode(JSON.stringify({ alg: 'RS256', typ: 'JWT' }));
  const claimSet = base64UrlEncode(JSON.stringify({
    iss: serviceAccount.client_email,
    scope: 'https://www.googleapis.com/auth/firebase.messaging',
    aud: 'https://oauth2.googleapis.com/token',
    iat: now,
    exp: now + 3600
  }));
  const unsigned = `${header}.${claimSet}`;
  const signer = createSign('RSA-SHA256');

  signer.update(unsigned);
  signer.end();

  const signature = base64UrlEncode(signer.sign(serviceAccount.private_key));
  const assertion = `${unsigned}.${signature}`;
  const response = await fetch('https://oauth2.googleapis.com/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
      assertion
    })
  });

  if (!response.ok) {
    throw new Error(`FCM OAuth token request failed with status ${response.status}`);
  }

  const payload = await response.json() as { access_token: string; expires_in: number };

  cachedFcmAccessToken = {
    token: payload.access_token,
    expiresAt: Date.now() + payload.expires_in * 1000
  };

  return payload.access_token;
}

async function sendFcmMessage(token: string, notification: PushNotificationPayload): Promise<void> {
  const serviceAccount = resolveFcmServiceAccount();

  if (!serviceAccount) {
    console.warn('[push] FCM credentials are not configured; skipping Android dispatch');
    return;
  }

  const accessToken = await getFcmAccessToken(serviceAccount);
  const response = await fetch(
    `https://fcm.googleapis.com/v1/projects/${serviceAccount.project_id}/messages:send`,
    {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${accessToken}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        message: {
          token,
          notification: {
            title: notification.title,
            body: notification.body
          },
          data: notification.data ?? {}
        }
      })
    }
  );

  if (!response.ok) {
    const errorBody = await response.text();

    throw new Error(`FCM dispatch failed (${response.status}): ${errorBody}`);
  }
}

function resolveApnsConfig(): {
  key: string;
  keyId: string;
  teamId: string;
  bundleId: string;
} | null {
  const keyPath = process.env.APNS_KEY_PATH?.trim();
  const keyId = process.env.APNS_KEY_ID?.trim();
  const teamId = process.env.APNS_TEAM_ID?.trim();
  const bundleId = process.env.APNS_BUNDLE_ID?.trim() || 'com.metoyou.app';

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

  return {
    key: fs.readFileSync(path.resolve(keyPath), 'utf8'),
    keyId,
    teamId,
    bundleId
  };
}

function buildApnsJwt(config: { key: string; keyId: string; teamId: string }): string {
  const now = Math.floor(Date.now() / 1000);
  const header = base64UrlEncode(JSON.stringify({ alg: 'ES256', kid: config.keyId }));
  const claims = base64UrlEncode(JSON.stringify({ iss: config.teamId, iat: now }));
  const unsigned = `${header}.${claims}`;
  const privateKey = createPrivateKey(config.key);
  const signature = sign('sha256', Buffer.from(unsigned), {
    key: privateKey,
    dsaEncoding: 'ieee-p1363'
  });

  return `${unsigned}.${base64UrlEncode(signature)}`;
}

async function sendApnsMessage(deviceToken: string, notification: PushNotificationPayload): Promise<void> {
  const config = resolveApnsConfig();

  if (!config) {
    console.warn('[push] APNs credentials are not configured; skipping iOS dispatch');
    return;
  }

  const host = process.env.APNS_USE_SANDBOX === 'true'
    ? 'api.sandbox.push.apple.com'
    : 'api.push.apple.com';
  const jwt = buildApnsJwt(config);
  const payload = JSON.stringify({
    aps: {
      alert: {
        title: notification.title,
        body: notification.body
      },
      sound: 'default'
    },
    ...notification.data
  });

  await new Promise<void>((resolve, reject) => {
    const client = http2.connect(`https://${host}`);
    const request = client.request({
      ':method': 'POST',
      ':path': `/3/device/${deviceToken}`,
      authorization: `bearer ${jwt}`,
      'apns-topic': config.bundleId,
      'apns-push-type': 'alert',
      'content-type': 'application/json'
    });

    request.setEncoding('utf8');
    request.on('response', (headers) => {
      const status = Number(headers[':status'] ?? 0);

      if (status >= 200 && status < 300) {
        resolve();
        return;
      }

      let body = '';

      request.on('data', (chunk) => {
        body += chunk;
      });

      request.on('end', () => {
        reject(new Error(`APNs dispatch failed (${status}): ${body}`));
      });
    });

    request.on('error', reject);
    request.end(payload);
    client.on('error', reject);
    request.on('close', () => client.close());
  });
}

export async function upsertDeviceToken(input: {
  userId: string;
  platform: 'ios' | 'android';
  token: string;
}): Promise<void> {
  const repository = getDataSource().getRepository(DeviceTokenEntity);
  const id = `${input.userId}:${input.platform}:${input.token}`;
  const record = repository.create({
    id,
    userId: input.userId,
    platform: input.platform,
    token: input.token,
    updatedAt: Date.now()
  });

  await repository.save(record);
}

export async function listDeviceTokensForUser(userId: string): Promise<DeviceTokenEntity[]> {
  return getDataSource()
    .getRepository(DeviceTokenEntity)
    .find({ where: { userId } });
}

export async function dispatchPushToUser(
  userId: string,
  notification: PushNotificationPayload
): Promise<void> {
  const tokens = await listDeviceTokensForUser(userId);

  await Promise.all(tokens.map(async (record) => {
    try {
      if (record.platform === 'android') {
        await sendFcmMessage(record.token, notification);
        return;
      }

      await sendApnsMessage(record.token, notification);
    } catch (error) {
      console.error('[push] Failed to dispatch to token', {
        userId,
        platform: record.platform,
        error
      });
    }
  }));
}
