import type { ChatMessage, Transaction } from "../shared/types";
import type { Session } from "./types";
import { createPrivateKey, sign } from "crypto";
import { existsSync, readFileSync } from "fs";
import { connect } from "node:http2";

type Platform = "ios" | "android";

type PushMessage = {
  to: string;
  platform: Platform;
  title?: string;
  body: string;
  data?: Record<string, string>;
};

type PlayerTokens = {
  ios: Set<string>;
  android: Set<string>;
};

const tokensBySession = new Map<string, Map<string, PlayerTokens>>();

const apnsKeyId = process.env.APNS_KEY_ID || "";
const apnsTeamId = process.env.APNS_TEAM_ID || "";
const apnsBundleId = process.env.APNS_BUNDLE_ID || "";
const apnsEnv = process.env.APNS_ENV === "production" ? "production" : "sandbox";
const apnsPrivateKey = process.env.APNS_PRIVATE_KEY || "";
const apnsPrivateKeyPath = process.env.APNS_PRIVATE_KEY_PATH || "";

const fcmProjectId = process.env.FCM_PROJECT_ID || "";
const fcmClientEmail = process.env.FCM_CLIENT_EMAIL || "";
const fcmPrivateKey = process.env.FCM_PRIVATE_KEY || "";
const fcmServerKey = process.env.FCM_SERVER_KEY || "";
const pushDebug = Boolean(process.env.PUSH_DEBUG);

let cachedApnsToken: { value: string; issuedAt: number } | null = null;
let cachedFcmToken: { value: string; expiresAt: number } | null = null;
let cachedApnsPrivateKey: string | null = null;

function formatMoney(amount: number): string {
  const value = new Intl.NumberFormat("en-US", { maximumFractionDigits: 0 }).format(
    Math.abs(amount),
  );
  return `₦${value}`;
}

function formatReason(note?: string | null): string {
  const trimmed = note?.trim();
  return trimmed && trimmed.length > 0 ? trimmed : "No reason provided";
}

function compactText(value: string, maxLength: number): string {
  const clean = value.replace(/\s+/g, " ").trim();
  if (clean.length <= maxLength) return clean;
  return `${clean.slice(0, maxLength - 1)}…`;
}

function getSessionTokens(
  sessionId: string,
  playerIds: string[],
  platform: Platform,
): string[] {
  const sessionTokens = tokensBySession.get(sessionId);
  if (!sessionTokens) return [];
  const combined = new Set<string>();
  playerIds.forEach((playerId) => {
    const tokens = sessionTokens.get(playerId);
    if (!tokens) return;
    const bucket = platform === "ios" ? tokens.ios : tokens.android;
    bucket.forEach((token) => combined.add(token));
  });
  return Array.from(combined);
}

function debugSessionTokens(sessionId: string): void {
  const sessionTokens = tokensBySession.get(sessionId);
  if (!sessionTokens) {
    console.log(`[push][debug] session=${sessionId} tokens=none`);
    return;
  }
  const dump: Record<string, { ios: string[]; android: string[] }> = {};
  sessionTokens.forEach((tokens, playerId) => {
    dump[playerId] = {
      ios: Array.from(tokens.ios),
      android: Array.from(tokens.android),
    };
  });
  console.log(`[push][debug] session=${sessionId} tokens=${JSON.stringify(dump)}`);
}

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

function normalizeKey(value: string): string {
  if (!value) return value;
  return value.includes("\\n") ? value.replace(/\\n/g, "\n") : value;
}

function normalizeData(data?: Record<string, unknown>): Record<string, string> | undefined {
  if (!data) return undefined;
  const entries = Object.entries(data).map(([key, value]) => [key, `${value ?? ""}`]);
  return Object.fromEntries(entries);
}

function resolveApnsPrivateKey(): string {
  if (cachedApnsPrivateKey !== null) return cachedApnsPrivateKey;
  const candidates = [apnsPrivateKey, apnsPrivateKeyPath].filter(Boolean);
  let resolved = "";
  for (const candidate of candidates) {
    if (candidate.includes("BEGIN")) {
      resolved = candidate;
      break;
    }
    if (existsSync(candidate)) {
      try {
        resolved = readFileSync(candidate, "utf8");
        break;
      } catch {
        // Ignore read failures; fallback to raw string.
      }
    }
  }
  cachedApnsPrivateKey = resolved || apnsPrivateKey;
  return cachedApnsPrivateKey;
}

function createApnsJwt(): string | null {
  const resolvedKey = resolveApnsPrivateKey();
  if (!apnsKeyId || !apnsTeamId || !apnsBundleId || !resolvedKey) {
    if (pushDebug) {
      const missing = [
        !apnsKeyId && "APNS_KEY_ID",
        !apnsTeamId && "APNS_TEAM_ID",
        !apnsBundleId && "APNS_BUNDLE_ID",
        !resolvedKey && "APNS_PRIVATE_KEY",
      ]
        .filter(Boolean)
        .join(", ");
      console.log(`[push][apns] skipped: missing ${missing || "credentials"}`);
    }
    return null;
  }
  const now = Math.floor(Date.now() / 1000);
  if (cachedApnsToken && now - cachedApnsToken.issuedAt < 50 * 60) {
    return cachedApnsToken.value;
  }
  const header = base64UrlEncode(JSON.stringify({ alg: "ES256", kid: apnsKeyId }));
  const payload = base64UrlEncode(JSON.stringify({ iss: apnsTeamId, iat: now }));
  const unsigned = `${header}.${payload}`;
  const key = createPrivateKey(normalizeKey(resolvedKey));
  const signature = sign("SHA256", Buffer.from(unsigned), {
    key,
    dsaEncoding: "ieee-p1363",
  });
  const token = `${unsigned}.${base64UrlEncode(signature)}`;
  cachedApnsToken = { value: token, issuedAt: now };
  return token;
}

async function sendApnsMessage(message: PushMessage): Promise<void> {
  let jwt: string | null = null;
  try {
    jwt = createApnsJwt();
  } catch (error) {
    if (pushDebug) {
      console.warn("[push][apns] failed to build JWT", error);
    }
    return;
  }
  if (!jwt) return;
  if (pushDebug) {
    const shortToken = message.to.slice(0, 12);
    console.log(`[push][apns] sending token=${shortToken}… env=${apnsEnv}`);
  }
  const payload: Record<string, unknown> = {
    aps: {
      alert: { title: message.title ?? "Negopoly", body: message.body },
      sound: "default",
    },
    ...message.data,
  };
  const host =
    apnsEnv === "production" ? "api.push.apple.com" : "api.sandbox.push.apple.com";
  await new Promise<void>((resolve) => {
    const client = connect(`https://${host}`);
    let responseStatus: number | null = null;
    let responseText = "";
    const req = client.request({
      ":method": "POST",
      ":path": `/3/device/${message.to}`,
      authorization: `bearer ${jwt}`,
      "apns-topic": apnsBundleId,
      "apns-push-type": "alert",
      "apns-priority": "10",
    });

    const timeout = setTimeout(() => {
      if (pushDebug) {
        console.warn("[push][apns] request timeout");
      }
      try {
        req.close();
      } catch {
        // Ignore stream close errors.
      }
      try {
        client.close();
      } catch {
        // Ignore client close errors.
      }
      resolve();
    }, 7000);

    req.setEncoding("utf8");
    req.on("response", (headers) => {
      const status = headers[":status"];
      if (typeof status === "number") {
        responseStatus = status;
      } else if (typeof status === "string") {
        const parsed = Number(status);
        responseStatus = Number.isNaN(parsed) ? null : parsed;
      }
    });
    req.on("data", (chunk) => {
      responseText += chunk;
    });
    req.on("end", () => {
      clearTimeout(timeout);
      if (pushDebug && responseStatus) {
        console.log(`[push][apns] ${responseStatus}`);
      }
      if (responseStatus && responseStatus >= 400) {
        console.warn(
          `[push][apns] ${responseStatus} ${responseText}`.trim(),
        );
      }
      try {
        client.close();
      } catch {
        // Ignore client close errors.
      }
      resolve();
    });
    req.on("error", (error) => {
      clearTimeout(timeout);
      if (pushDebug) {
        console.warn("[push][apns] request failed", error);
      }
      try {
        client.close();
      } catch {
        // Ignore client close errors.
      }
      resolve();
    });
    client.on("error", (error) => {
      clearTimeout(timeout);
      if (pushDebug) {
        console.warn("[push][apns] client error", error);
      }
      try {
        client.close();
      } catch {
        // Ignore client close errors.
      }
      resolve();
    });

    req.end(JSON.stringify(payload));
  });
}

async function getFcmAccessToken(): Promise<string | null> {
  if (!fcmProjectId || !fcmClientEmail || !fcmPrivateKey) {
    return null;
  }
  const now = Math.floor(Date.now() / 1000);
  if (cachedFcmToken && cachedFcmToken.expiresAt - now > 60) {
    return cachedFcmToken.value;
  }
  const header = base64UrlEncode(JSON.stringify({ alg: "RS256", typ: "JWT" }));
  const payload = base64UrlEncode(
    JSON.stringify({
      iss: fcmClientEmail,
      scope: "https://www.googleapis.com/auth/firebase.messaging",
      aud: "https://oauth2.googleapis.com/token",
      iat: now,
      exp: now + 3600,
    }),
  );
  const unsigned = `${header}.${payload}`;
  const key = createPrivateKey(normalizeKey(fcmPrivateKey));
  const signature = sign("RSA-SHA256", Buffer.from(unsigned), key);
  const assertion = `${unsigned}.${base64UrlEncode(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,
    }).toString(),
  });
  if (!response.ok) return null;
  const data = (await response.json()) as { access_token?: string; expires_in?: number };
  if (!data.access_token) return null;
  cachedFcmToken = {
    value: data.access_token,
    expiresAt: now + (data.expires_in ?? 3600),
  };
  return data.access_token;
}

async function sendFcmMessage(message: PushMessage): Promise<void> {
  if (fcmServerKey) {
    const response = await fetch("https://fcm.googleapis.com/fcm/send", {
      method: "POST",
      headers: {
        Authorization: `key=${fcmServerKey}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        to: message.to,
        notification: { title: message.title ?? "Negopoly", body: message.body },
        data: message.data ?? {},
      }),
    });
    if (pushDebug) {
      console.log(`[push][fcm-legacy] ${response.status} ${response.statusText}`);
    }
    if (!response.ok) {
      const detail = await response.text().catch(() => "");
      console.warn(
        `[push][fcm-legacy] ${response.status} ${response.statusText} ${detail}`.trim(),
      );
    }
    return;
  }
  const accessToken = await getFcmAccessToken();
  if (!accessToken) return;
  const response = await fetch(
    `https://fcm.googleapis.com/v1/projects/${fcmProjectId}/messages:send`,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${accessToken}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        message: {
          token: message.to,
          notification: { title: message.title ?? "Negopoly", body: message.body },
          data: message.data ?? {},
        },
      }),
    },
  );
  if (pushDebug) {
    console.log(`[push][fcm] ${response.status} ${response.statusText}`);
  }
  if (!response.ok) {
    const detail = await response.text().catch(() => "");
    console.warn(
      `[push][fcm] ${response.status} ${response.statusText} ${detail}`.trim(),
    );
  }
}

async function sendPushMessages(messages: PushMessage[]): Promise<void> {
  if (messages.length === 0) return;
  for (const message of messages) {
    if (message.platform === "ios") {
      await sendApnsMessage(message);
    } else {
      await sendFcmMessage(message);
    }
  }
}

function queuePushMessages(messages: PushMessage[]): void {
  if (messages.length === 0) return;
  if (pushDebug) {
    const counts = messages.reduce(
      (acc, message) => {
        acc.total += 1;
        if (message.platform === "ios") acc.ios += 1;
        if (message.platform === "android") acc.android += 1;
        return acc;
      },
      { total: 0, ios: 0, android: 0 },
    );
    console.log(
      `[push][debug] queued messages total=${counts.total} ios=${counts.ios} android=${counts.android}`,
    );
  }
  void sendPushMessages(messages).catch(() => {
    // Ignore push failures to avoid breaking gameplay flow.
  });
}

export function registerPushToken(
  sessionId: string,
  playerId: string,
  token: string,
  platform: Platform,
): void {
  const normalized = token.trim();
  if (!normalized) return;
  let sessionTokens = tokensBySession.get(sessionId);
  if (!sessionTokens) {
    sessionTokens = new Map();
    tokensBySession.set(sessionId, sessionTokens);
  }
  let playerTokens = sessionTokens.get(playerId);
  if (!playerTokens) {
    playerTokens = { ios: new Set(), android: new Set() };
    sessionTokens.set(playerId, playerTokens);
  }
  if (platform === "ios") {
    playerTokens.ios.add(normalized);
  } else {
    playerTokens.android.add(normalized);
  }
  if (pushDebug) {
    console.log(`[push] registered ${platform} token for ${sessionId}:${playerId}`);
  }
}

export function clearPushTokensForSession(sessionId: string): void {
  tokensBySession.delete(sessionId);
}

export function notifyTransaction(session: Session, transaction: Transaction): void {
  const reason = formatReason(transaction.note);
  if (transaction.kind === "banker_adjust") {
    const targetId = transaction.toId;
    if (!targetId) return;
    if (pushDebug) {
      debugSessionTokens(session.id);
      console.log(`[push][debug] banker_adjust target=${targetId}`);
    }
    const signed = `${transaction.amount >= 0 ? "+" : "-"}${formatMoney(transaction.amount)}`;
    const body = `Balance adjusted: ${signed} : ${reason}`;
    const data = normalizeData({ type: "transactions", sessionId: session.id });
    const iosTokens = getSessionTokens(session.id, [targetId], "ios");
    const androidTokens = getSessionTokens(session.id, [targetId], "android");
    if (pushDebug && iosTokens.length + androidTokens.length === 0) {
      console.log(`[push] no tokens for banker_adjust target=${targetId}`);
    }
    const messages = [
      ...iosTokens.map((token) => ({
        to: token,
        platform: "ios" as const,
        title: "Negopoly",
        body,
        data,
      })),
      ...androidTokens.map((token) => ({
        to: token,
        platform: "android" as const,
        title: "Negopoly",
        body,
        data,
      })),
    ];
    queuePushMessages(messages);
    return;
  }

  const fromId = transaction.fromId;
  const toId = transaction.toId;
  if (!fromId || !toId) return;

  const fromName = session.players.get(fromId)?.name ?? "Player";
  const toName = session.players.get(toId)?.name ?? "Player";
  const amount = formatMoney(transaction.amount);
  const receiveBody = `Received ${amount} from ${fromName}: ${reason}`;
  const sentBody = `Sent ${amount} to ${toName}: ${reason}`;
  const data = normalizeData({ type: "transactions", sessionId: session.id });

  const outgoingTokensIos = getSessionTokens(session.id, [fromId], "ios");
  const incomingTokensIos = getSessionTokens(session.id, [toId], "ios");
  const outgoingTokensAndroid = getSessionTokens(session.id, [fromId], "android");
  const incomingTokensAndroid = getSessionTokens(session.id, [toId], "android");
  if (pushDebug) {
    debugSessionTokens(session.id);
    console.log(
      `[push][debug] transaction kind=${transaction.kind} from=${fromId} to=${toId} outgoing=${outgoingTokensIos.length + outgoingTokensAndroid.length} incoming=${incomingTokensIos.length + incomingTokensAndroid.length}`,
    );
    if (outgoingTokensIos.length + outgoingTokensAndroid.length === 0) {
      console.log(`[push] no tokens for transfer sender=${fromId}`);
    }
    if (incomingTokensIos.length + incomingTokensAndroid.length === 0) {
      console.log(`[push] no tokens for transfer recipient=${toId}`);
    }
  }

  const messages = [
    ...outgoingTokensIos.map((token) => ({
      to: token,
      platform: "ios" as const,
      title: "Negopoly",
      body: sentBody,
      data,
    })),
    ...incomingTokensIos.map((token) => ({
      to: token,
      platform: "ios" as const,
      title: "Negopoly",
      body: receiveBody,
      data,
    })),
    ...outgoingTokensAndroid.map((token) => ({
      to: token,
      platform: "android" as const,
      title: "Negopoly",
      body: sentBody,
      data,
    })),
    ...incomingTokensAndroid.map((token) => ({
      to: token,
      platform: "android" as const,
      title: "Negopoly",
      body: receiveBody,
      data,
    })),
  ];

  queuePushMessages(messages);
}

export function notifyChat(session: Session, message: ChatMessage): void {
  const sender = session.players.get(message.fromId);
  const senderName = sender?.name ?? "Player";
  const chatId = message.groupId ?? "global";
  let groupName = "Global";
  let recipients: string[] = [];

  if (message.groupId === null) {
    recipients = Array.from(session.players.keys());
  } else {
    const group = session.groups.find((item) => item.id === message.groupId);
    if (!group) return;
    groupName = group.name;
    recipients = group.memberIds.slice();
  }

  recipients = recipients.filter((id) => id !== message.fromId);
  if (recipients.length === 0) return;

  const preview = compactText(message.body, 90);
  const body = `${senderName}#${groupName}: ${preview}`;
  const data = normalizeData({ type: "chat", sessionId: session.id, chatId });
  const tokensIos = getSessionTokens(session.id, recipients, "ios");
  const tokensAndroid = getSessionTokens(session.id, recipients, "android");
  if (pushDebug && tokensIos.length + tokensAndroid.length === 0) {
    console.log(`[push] no tokens for chat recipients=${recipients.join(",")}`);
  }
  const messages = [
    ...tokensIos.map((token) => ({
      to: token,
      platform: "ios" as const,
      title: "Negopoly",
      body,
      data,
    })),
    ...tokensAndroid.map((token) => ({
      to: token,
      platform: "android" as const,
      title: "Negopoly",
      body,
      data,
    })),
  ];
  queuePushMessages(messages);
}
