import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
// `web-push` is CommonJS. A namespace import is required in the production
// CommonJS build; a synthetic default exists only to the type checker.
import * as webPush from 'web-push';
import type { ChannelVisibility } from './community.repository';
import {
  CommunityPushRepository,
  type StoredPushSubscription,
} from './community-push.repository';

interface PublishedPostNotification {
  id: string;
  title: string;
  excerpt: string | null;
  channelName: string;
  visibility: ChannelVisibility;
}

@Injectable()
export class CommunityPushService {
  private readonly logger = new Logger(CommunityPushService.name);
  private readonly publicKey: string | null;
  private readonly enabled: boolean;

  constructor(
    config: ConfigService,
    private readonly repository: CommunityPushRepository,
  ) {
    const publicKey = config.get<string>('VAPID_PUBLIC_KEY')?.trim() || null;
    const privateKey = config.get<string>('VAPID_PRIVATE_KEY')?.trim() || null;
    const subject = config.get<string>('VAPID_SUBJECT')?.trim() || null;
    this.publicKey = publicKey;
    this.enabled = Boolean(publicKey && privateKey && subject);

    if (this.enabled) {
      webPush.setVapidDetails(subject!, publicKey!, privateKey!);
    } else {
      this.logger.warn('Community push is disabled because VAPID configuration is incomplete.');
    }
  }

  configuration() {
    return { enabled: this.enabled, publicKey: this.enabled ? this.publicKey : null };
  }

  subscriptionsForUser(ownerId: string) {
    return this.repository.subscriptionsForUser(ownerId);
  }

  async save(ownerId: string, subscription: StoredPushSubscription): Promise<void> {
    await this.repository.save(ownerId, subscription);
  }

  async remove(ownerId: string, endpoint: string): Promise<void> {
    await this.repository.remove(ownerId, endpoint);
  }

  /** Publication succeeds even if a remote push endpoint is temporarily down. */
  async notifyPostPublished(post: PublishedPostNotification): Promise<void> {
    if (!this.enabled) return;
    let recipients: Awaited<ReturnType<CommunityPushRepository['recipients']>>;
    try {
      recipients = await this.repository.recipients(post.visibility);
    } catch {
      this.logger.warn('Community push recipients could not be loaded.');
      return;
    }
    if (!recipients.length) return;

    const excerpt = compact(post.excerpt || 'Jetzt im CK Terminal lesen.', 120);
    const payload = JSON.stringify({
      title: 'CK Community · Neuer Beitrag',
      body: `${compact(post.title, 90)} · ${post.channelName}\n${excerpt}`,
      icon: '/icon.png',
      badge: '/apple-icon.png',
      tag: `ck-community-post-${post.id}`,
      url: `/community#p=${post.id}`,
      // Ausdrücklich mitgeschickt: der Service Worker reicht die Kennung an
      // eine bereits geöffnete Ansicht weiter, weil ein Wechsel des Ankers
      // allein dort nichts auslöst (siehe public/sw.js).
      postId: post.id,
    });

    const deliveries = await Promise.allSettled(
      recipients.map(async ({ ownerId, subscription }) => {
        try {
          await webPush.sendNotification(subscription, payload, {
            TTL: 24 * 60 * 60,
            timeout: 5_000,
            urgency: 'high',
            topic: `community-${post.id.slice(0, 20)}`,
          });
        } catch (error) {
          if (isExpiredSubscription(error)) {
            await this.repository.remove(ownerId, subscription.endpoint);
            return;
          }
          throw error;
        }
      }),
    );

    const failed = deliveries.filter((delivery) => delivery.status === 'rejected').length;
    if (failed) {
      this.logger.warn(`Community push delivery failed for ${failed} recipient(s).`);
    }
  }
}

function compact(value: string, maxLength: number): string {
  const text = value.replace(/\s+/g, ' ').trim();
  return text.length <= maxLength ? text : `${text.slice(0, maxLength - 1).trimEnd()}…`;
}

function isExpiredSubscription(error: unknown): boolean {
  if (!error || typeof error !== 'object') return false;
  const statusCode = (error as { statusCode?: unknown }).statusCode;
  return statusCode === 404 || statusCode === 410;
}
