import { Injectable, Inject, forwardRef } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { ConfigService } from '@nestjs/config';
import { Kysely } from 'kysely';
import { DB } from '../../database/database.type';
import { DeviceService } from '../devices/device.service';
import { AppConfig } from '../../config/configs';
import { Logger } from 'pino-nestjs';
import { firstValueFrom } from 'rxjs';
import 'dotenv/config';

export const PUSH_MAX_ATTEMPTS = 3;
export const PUSH_RECEIPT_DELAY_MS = 30_000;
export const EXPO_PUSH_URL = process.env.EXPO_PUSH_URL;
export const EXPO_RECEIPT_URL = process.env.EXPO_RECEIPT_URL;
export const EXPO_ANDROID_CHANNEL_ID = 'furfinder-alerts';
export const EXPO_PUSH_TOKEN_REGEX = /^(ExponentPushToken|ExpoPushToken)\[[^\]]+\]$/;

export type PushTicket =
  | { status: 'ok'; id: string }
  | { status: 'error'; message: string; details?: { error?: string } };

export type PushReceipt =
  | { status: 'ok' }
  | { status: 'error'; message: string; details?: { error?: string } };

export const PERMANENT_ERRORS = new Set(['DeviceNotRegistered', 'InvalidCredentials']);

@Injectable()
export class PushService {
  constructor(
    private readonly httpService: HttpService,
    @Inject(forwardRef(() => DeviceService))
    private readonly deviceService: DeviceService,
    private readonly configService: ConfigService<AppConfig, true>,
    private readonly db: Kysely<DB>,
    private readonly logger: Logger,
  ) {}

  private isExpoPushToken(token: string): boolean {
    return EXPO_PUSH_TOKEN_REGEX.test(token.trim());
  }

  private getExpoRequestHeaders(): Record<string, string> {
    const headers: Record<string, string> = {
      'Content-Type': 'application/json',
      Accept: 'application/json',
    };
    const accessToken = this.configService.get('push.accessToken', { infer: true });
    if (accessToken) {
      headers.Authorization = `Bearer ${accessToken}`;
    }
    return headers;
  }

  async clearPushToken(userId: string, pushToken: string): Promise<void> {
    try {
      // Clear token from devices table
      await this.deviceService.clearPushTokenByValue(pushToken);

      // Clear token from legacy column in users table
      await this.db
        .updateTable('users')
        .set({ push_token: null })
        .where('id', '=', userId)
        .where('push_token', '=', pushToken)
        .where('deleted_at', 'is', null)
        .execute();

      this.logger.log(`[Push] Cleared stale token for user ${userId}`);
    } catch (err) {
      this.logger.error('[Push] Failed to clear stale token:', err);
    }
  }

  async checkPushReceipt(ticketId: string, userId: string, pushToken: string): Promise<void> {
    try {
      const response = await firstValueFrom(
        this.httpService.post(
          this.configService.get('push.expoReceiptUrl', { infer: true }),
          { ids: [ticketId] },
          {
            headers: {
              'Content-Type': 'application/json',
              Accept: 'application/json',
            },
          },
        ),
      );

      const json = response.data as { data: Record<string, PushReceipt> };
      const receipt = json?.data?.[ticketId];

      if (receipt?.status === 'error') {
        const errorType = (receipt as any)?.details?.error as string | undefined;
        this.logger.error(`[Push] Receipt error for ticket ${ticketId}: ${receipt.message} (${errorType})`);
        if (errorType && PERMANENT_ERRORS.has(errorType)) {
          await this.clearPushToken(userId, pushToken);
        }
      }
    } catch (err) {
      this.logger.error('[Push] Receipt check error:', err);
    }
  }

  async sendPushNotification(
    pushToken: string | null,
    userId: string,
    title: string,
    body: string,
    data?: object,
  ): Promise<void> {
    if (!pushToken || !this.isExpoPushToken(pushToken)) return;
    const normalizedToken = pushToken.trim();

    let ticketId: string | null = null;

    for (let attempt = 1; attempt <= PUSH_MAX_ATTEMPTS; attempt++) {
      try {
        const response = await firstValueFrom(
          this.httpService.post(
            this.configService.get('push.expoPushUrl', { infer: true }),
            {
              to: normalizedToken,
              title,
              body,
              data: data || {},
              badge: 1,
              sound: 'default',
              priority: 'high',
              channelId: EXPO_ANDROID_CHANNEL_ID,
              ttl: 3600,
            },
            {
              headers: {
                ...this.getExpoRequestHeaders(),
                'Accept-Encoding': 'gzip, deflate',
              },
            },
          ),
        );

        const json = response.data as { data: PushTicket | PushTicket[] };
        const tickets = Array.isArray(json?.data) ? json.data : [json?.data];
        const ticket = tickets[0];

        if (ticket?.status === 'error') {
          const errorType = (ticket as any)?.details?.error as string | undefined;
          this.logger.error(`[Push] Send error for user ${userId}: ${ticket.message} (${errorType})`);
          if (errorType && PERMANENT_ERRORS.has(errorType)) {
            await this.clearPushToken(userId, normalizedToken);
          }
          return;
        }

        if (ticket?.status === 'ok' && (ticket as any).id) {
          ticketId = (ticket as any).id;
          this.logger.log(`[Push] Sent to user ${userId}, ticket ${ticketId}`);
        }

        break;
      } catch (err) {
        this.logger.error(`[Push] Attempt ${attempt}/${PUSH_MAX_ATTEMPTS} failed for user ${userId}:`, err);
        if (attempt < PUSH_MAX_ATTEMPTS) {
          await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));
        }
      }
    }

    if (ticketId) {
      const capturedTicketId = ticketId;
      setTimeout(() => {
        this.checkPushReceipt(capturedTicketId, userId, normalizedToken).catch(() => {});
      }, PUSH_RECEIPT_DELAY_MS);
    }
  }
}
