import {
  MatrixClient,
  IPusherRequest,
  IPushRule,
  PushRuleKind,
  ConditionKind,
  PushRuleActionName,
  TweakName,
} from 'matrix-js-sdk';
import { isNativePlatform } from './capacitor';

export type PushPlatform = 'web' | 'fcm';

export const getPushPlatform = (): PushPlatform => (isNativePlatform() ? 'fcm' : 'web');

export const PUSH_APP_IDS = {
  web: 'chat.vojo.app.web',
  fcm: 'chat.vojo.app.fcm',
} as const;

export const PUSH_ENABLED_KEY = 'vojo_push_enabled';
export const PUSH_STATE_CHANGE_EVENT = 'vojo:pushStateChange';

export const isPushEnabled = (): boolean => localStorage.getItem(PUSH_ENABLED_KEY) === 'true';

export const setPushEnabled = (enabled: boolean): void => {
  const prev = isPushEnabled();
  if (enabled) localStorage.setItem(PUSH_ENABLED_KEY, 'true');
  else localStorage.removeItem(PUSH_ENABLED_KEY);
  // Fan out to any UI that mirrors the flag (settings toggle, soft prompt, etc.)
  // The `storage` event only fires cross-tab, so in-tab observers need this.
  if (prev !== enabled && typeof window !== 'undefined') {
    window.dispatchEvent(new CustomEvent(PUSH_STATE_CHANGE_EVENT));
  }
};

export function urlBase64ToUint8Array(base64String: string): Uint8Array {
  const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
  const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
  const rawData = atob(base64);
  const output = new Uint8Array(rawData.length);
  for (let i = 0; i < rawData.length; i += 1) output[i] = rawData.charCodeAt(i);
  return output;
}

const arrayBufferToBase64Url = (buf: ArrayBuffer): string => {
  const bytes = new Uint8Array(buf);
  let binary = '';
  for (let i = 0; i < bytes.byteLength; i += 1) binary += String.fromCharCode(bytes[i]);
  return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
};

/* eslint-disable no-await-in-loop */
async function withRetry<T>(fn: () => Promise<T>, attempts = 3): Promise<T> {
  let lastErr: unknown;
  for (let i = 0; i < attempts; i += 1) {
    try {
      return await fn();
    } catch (err) {
      lastErr = err;
      if (i < attempts - 1) {
        await new Promise((r) => {
          setTimeout(r, 2 ** i * 500);
        });
      }
    }
  }
  throw lastErr;
}
/* eslint-enable no-await-in-loop */

export type PusherIds = {
  pushkey: string;
  appId: string;
};

export const RTC_RING_PUSH_RULE_ID = 'chat.vojo.rtc.ring';
export const RTC_RING_PUSH_RULE_ID_STABLE = 'chat.vojo.rtc.ring.stable';

// Matrix default push rules don't cover MSC4075 RTC ring events, so without
// an explicit Override rule the homeserver never dispatches RTC ring events
// to Sygnal — background/killed devices silently miss incoming DM calls.
//
// Two rules are registered in parallel: one against the unstable
// `org.matrix.msc4075.rtc.notification` event type that current matrix-js-sdk
// (and our send-side cleartext bypass in `CallWidgetDriver`) emits, and one
// against the stable `m.rtc.notification` name so peers running an upgraded
// SDK with the stable promotion don't silently miss us. EventMatch only
// matches the outer cleartext event type, which is why our send-side relies
// on the cleartext bypass — this rule only fires on cleartext ring events.
//
// Idempotent: Synapse returns 200 OK on PUT with an identical body, so calling
// these on every pusher (re)registration and on lifecycle startup is cheap.
// Failures are logged but not thrown — a transient /pushrules 5xx shouldn't
// break pusher setup (foreground path via useIncomingRtcNotifications keeps
// working regardless of whether the rule made it to the server).
const ringRuleBody = (
  eventTypePattern: string
): Pick<IPushRule, 'actions' | 'conditions' | 'pattern'> => ({
  conditions: [
    {
      kind: ConditionKind.EventMatch,
      key: 'type',
      pattern: eventTypePattern,
    },
    {
      kind: ConditionKind.EventMatch,
      key: 'content.notification_type',
      pattern: 'ring',
    },
  ],
  actions: [
    PushRuleActionName.Notify,
    { set_tweak: TweakName.Sound, value: 'ring' },
    { set_tweak: TweakName.Highlight, value: true },
  ],
});

export async function ensureRtcRingPushRule(mx: MatrixClient): Promise<void> {
  try {
    await mx.addPushRule(
      'global',
      PushRuleKind.Override,
      RTC_RING_PUSH_RULE_ID,
      ringRuleBody('org.matrix.msc4075.rtc.notification')
    );
  } catch (err) {
    // eslint-disable-next-line no-console
    console.warn('[push] ensureRtcRingPushRule (unstable) failed:', err);
  }
  try {
    await mx.addPushRule(
      'global',
      PushRuleKind.Override,
      RTC_RING_PUSH_RULE_ID_STABLE,
      ringRuleBody('m.rtc.notification')
    );
  } catch (err) {
    // eslint-disable-next-line no-console
    console.warn('[push] ensureRtcRingPushRule (stable) failed:', err);
  }
}

// Symmetric cleanup for `useDisablePushNotifications`. The rules live in
// account data so they would otherwise outlive the pusher — and other logged-in
// clients (Element, a second Vojo session) would keep applying the ring
// tweaks after the user explicitly turned push off here. 404 on delete of an
// absent rule lands in the same warn path — that's the "already gone" case,
// not a failure worth surfacing.
export async function removeRtcRingPushRule(mx: MatrixClient): Promise<void> {
  try {
    await mx.deletePushRule('global', PushRuleKind.Override, RTC_RING_PUSH_RULE_ID);
  } catch (err) {
    // eslint-disable-next-line no-console
    console.warn('[push] removeRtcRingPushRule (unstable) failed:', err);
  }
  try {
    await mx.deletePushRule('global', PushRuleKind.Override, RTC_RING_PUSH_RULE_ID_STABLE);
  } catch (err) {
    // eslint-disable-next-line no-console
    console.warn('[push] removeRtcRingPushRule (stable) failed:', err);
  }
}

export async function registerWebPusher(
  mx: MatrixClient,
  subscription: PushSubscription,
  gatewayUrl: string,
  appId: string
): Promise<PusherIds> {
  const p256dh = subscription.getKey('p256dh');
  const auth = subscription.getKey('auth');
  if (!p256dh || !auth) throw new Error('PushSubscription is missing required keys');

  // Sygnal webpush spec (https://github.com/matrix-org/sygnal/blob/main/docs/applications.md):
  // pushkey = p256dh (base64url), endpoint/auth go inside data.
  // `format: 'event_id_only'` belongs at top-level of `data` per Matrix pusher spec —
  // Sygnal reads it there to strip content before shipping the push. The SW then pulls
  // content via authed Matrix API, so nothing sensitive flows through the push gateway.
  // `events_only: true` tells Sygnal to skip unread-count-only pushes (no event_id →
  // nothing useful for the SW to display, would produce generic "New message" spam).
  // append: true so we don't clobber pushers from other accounts that share the same
  // browser-level push subscription (one SW → one p256dh across accounts).
  const pushkey = arrayBufferToBase64Url(p256dh);

  await withRetry(() =>
    mx.setPusher({
      kind: 'http',
      app_id: appId,
      pushkey,
      app_display_name: 'Vojo Web',
      device_display_name: navigator.userAgent.slice(0, 120),
      lang: navigator.language || 'en',
      // SDK's pusher `data` type only declares `{ format?, url?, brand? }`.
      // Sygnal / UnifiedPush also read `endpoint`, `auth`, `events_only`; the
      // extra fields are forwarded to the gateway and required for web-push
      // delivery. Cast through `unknown` to drop the over-narrow SDK shape.
      data: {
        url: gatewayUrl,
        endpoint: subscription.endpoint,
        auth: arrayBufferToBase64Url(auth),
        format: 'event_id_only',
        events_only: true,
      } as unknown as { format?: string; url?: string; brand?: string },
      append: true,
    })
  );

  return { pushkey, appId };
}

export async function registerFcmPusher(
  mx: MatrixClient,
  fcmToken: string,
  gatewayUrl: string,
  appId: string,
  deviceName = 'Android'
): Promise<PusherIds> {
  // Intentionally NOT sending `format: 'event_id_only'` — we need the full payload
  // fields (room_name, sender_display_name, content_body, …) in FCM data. When the
  // app is killed, VojoFirebaseMessagingService (native Java) builds the system
  // notification straight from the data map; it can't easily re-auth against the
  // Matrix API to fetch event content the way the web Service Worker does.
  // Sygnal ships Matrix pushes as data-only FCM regardless of `format` — the field
  // controls which keys are present, not FCM priority/delivery.
  await withRetry(() =>
    mx.setPusher({
      kind: 'http',
      app_id: appId,
      pushkey: fcmToken,
      app_display_name: 'Vojo Android',
      device_display_name: deviceName,
      lang: navigator.language || 'en',
      data: {
        url: gatewayUrl,
      },
      append: false,
    })
  );

  return { pushkey: fcmToken, appId };
}

export async function unregisterPusher(
  mx: MatrixClient,
  pushkey: string,
  appId: string
): Promise<void> {
  await mx.setPusher({
    kind: null,
    app_id: appId,
    pushkey,
  } as unknown as IPusherRequest);
}

const PUSHER_IDS_KEY = 'vojo_push_pusher_ids';

export const savePusherIds = (ids: PusherIds): void => {
  localStorage.setItem(PUSHER_IDS_KEY, JSON.stringify(ids));
};

export const loadPusherIds = (): PusherIds | undefined => {
  const raw = localStorage.getItem(PUSHER_IDS_KEY);
  if (!raw) return undefined;
  try {
    const parsed = JSON.parse(raw);
    if (typeof parsed?.pushkey === 'string' && typeof parsed?.appId === 'string') return parsed;
  } catch {
    /* noop */
  }
  return undefined;
};

export const clearPusherIds = (): void => {
  localStorage.removeItem(PUSHER_IDS_KEY);
};
