import { Injectable, HttpStatus, BadRequestException } from '@nestjs/common';
import { Kysely, sql } from 'kysely';
import { DB } from '@/database/database.type';
import { DeviceService } from '../devices/device.service';
import { PushService, EXPO_PUSH_TOKEN_REGEX } from '../push/push.service';
import { SendPushDto } from './notifications.dto';

@Injectable()
export class NotificationsService {
  constructor(
    private readonly db: Kysely<DB>,
    private readonly deviceService: DeviceService,
    private readonly pushService: PushService,
  ) {}

  /**
   * Latest 50 notifications. `read` is derived from either column for
   * backwards compatibility with older rows that only set `read_at`.
   */
  async getNotifications(user_id: string) {
    const result = await this.db
      .selectFrom('notifications')
      .select([
        'id',
        'user_id',
        'type',
        'title',
        'message',
        'report_id',
        'profile_id',
        'quick_snap_id',
        'source_event_id',
        'read_at',
        'created_at',
        sql<boolean>`(COALESCE(read, false) OR read_at IS NOT NULL)`.as('read'),
      ])
      .where('user_id', '=', user_id)
      .where('deleted_at', 'is', null)
      .orderBy('created_at', 'desc')
      .limit(50)
      .execute();

    return { status: true, statusCode: HttpStatus.OK, data: result };
  }

  async markAsRead(user_id: string, notificationId: string) {
    const result = await this.db
      .updateTable('notifications')
      .set({ read: true, read_at: sql`COALESCE(read_at, NOW())` })
      .where('id', '=', notificationId)
      .where('user_id', '=', user_id)
      .where('deleted_at', 'is', null)
      .returning('id')
      .executeTakeFirst();

    if (!result) {
      return { status: false, statusCode: HttpStatus.NOT_FOUND, message: 'Notification not found' };
    }
    return { status: true, statusCode: HttpStatus.OK, data: { message: 'Notification marked as read' } };
  }

  async markAllAsRead(user_id: string) {
    await this.db
      .updateTable('notifications')
      .set({ read: true, read_at: sql`COALESCE(read_at, NOW())` })
      .where('user_id', '=', user_id)
      .where('deleted_at', 'is', null)
      .execute();

    return { status: true, statusCode: HttpStatus.OK, data: { message: 'All notifications marked as read' } };
  }

  async deleteAllNotifications(user_id: string) {
    await this.db
      .updateTable('notifications')
      .set({ deleted_at: sql`NOW()` })
      .where('user_id', '=', user_id)
      .where('deleted_at', 'is', null)
      .execute();

    return { status: true, statusCode: HttpStatus.OK, data: { message: 'All notifications deleted' } };
  }

  /**
   * Manual push to a user, a device, a single token, or all anonymous devices.
   */
  async sendPush(sender_user_id: string, payload: SendPushDto) {
    const targetUserId = payload.user_id || sender_user_id;
    const targetDeviceId = payload.device_id || '';
    const toAnonymous = Boolean(payload.anonymous);
    const { title, message, data } = payload;
    const manualPushToken = String(payload.push_token || '').trim();

    if (!targetUserId && !targetDeviceId && !manualPushToken && !toAnonymous) {
      throw new BadRequestException('user_id, device_id, anonymous=true, or push_token is required');
    }

    const tokenSet = new Set<string>();

    if (manualPushToken) {
      if (!EXPO_PUSH_TOKEN_REGEX.test(manualPushToken)) {
        throw new BadRequestException('Invalid Expo push token format');
      }
      tokenSet.add(manualPushToken);
    }

    if (targetDeviceId) {
      const deviceTokens = await this.deviceService.getDevicePushTokens(targetDeviceId);
      deviceTokens.forEach((token) => tokenSet.add(token));
    }

    if (toAnonymous) {
      const anonTokens = await this.deviceService.getAnonymousPushTokens(500);
      anonTokens.forEach((token) => tokenSet.add(token));
    }

    if (targetUserId) {
      const userTokens = await this.deviceService.getUserPushTokens(targetUserId);
      userTokens.forEach((token) => tokenSet.add(token));
    }

    const tokens = Array.from(tokenSet);
    if (tokens.length === 0) {
      throw new BadRequestException('No push tokens available for the selected target');
    }

    for (const token of tokens) {
      await this.pushService.sendPushNotification(token, targetUserId || sender_user_id, title, message, data || {});
    }

    return {
      status: true,
      statusCode: HttpStatus.OK,
      data: {
        message: 'Push notification queued',
        target_user_id: targetUserId || null,
        target_device_id: targetDeviceId || null,
        anonymous: toAnonymous,
        tokens_count: tokens.length,
        title,
      },
    };
  }

  // ── Reward / referral notification helpers (consumed by Referrals reward engine) ──

  /**
   * Insert a notification, deduped by `(user_id, type, source_event_id)` when a
   * source event id is supplied. Pass a Kysely transaction to enlist in an
   * outer unit of work. Returns whether a new row was created.
   */
  async insertNotificationOnce(
    input: { user_id: string; type: string; title: string; message: string; sourceEventId?: string | null },
    client: Kysely<DB> = this.db,
  ): Promise<{ inserted: boolean; id: string | null }> {
    const sourceEventId = String(input.sourceEventId || '').trim() || null;

    if (sourceEventId) {
      const result = await sql<{ id: string }>`
        INSERT INTO notifications (user_id, type, title, message, source_event_id)
        VALUES (${input.user_id}, ${input.type}, ${input.title}, ${input.message}, ${sourceEventId})
        ON CONFLICT (user_id, type, source_event_id)
        WHERE source_event_id IS NOT NULL AND deleted_at IS NULL
        DO NOTHING
        RETURNING id
      `.execute(client);
      return { inserted: result.rows.length > 0, id: result.rows[0]?.id || null };
    }

    const result = await sql<{ id: string }>`
      INSERT INTO notifications (user_id, type, title, message)
      VALUES (${input.user_id}, ${input.type}, ${input.title}, ${input.message})
      RETURNING id
    `.execute(client);
    return { inserted: true, id: result.rows[0]?.id || null };
  }

  private async sendUserPush(
    user_id: string,
    payload: { type: string; title: string; message: string; route: string },
    data: Record<string, unknown>,
  ) {
    const pushTokens = await this.deviceService.getUserPushTokens(user_id);
    for (const pushToken of pushTokens) {
      await this.pushService.sendPushNotification(pushToken, user_id, payload.title, payload.message, {
        type: payload.type,
        route: payload.route,
        ...data,
      });
    }
  }

  async createSignupBonusNotification(
    user_id: string,
    months: number,
    premiumUntil?: Date | string | null,
  ) {
    const safeMonths = Math.max(1, Math.trunc(Number(months) || 1));
    const monthLabel = `${safeMonths} ${safeMonths === 1 ? 'month' : 'months'}`;
    const title = 'Free Premium unlocked';
    const message = `You received ${monthLabel} of free Premium for joining The Fur Finder.`;
    const route = '/settings/subscription';

    const notification = await this.insertNotificationOnce({
      user_id,
      type: 'signup_bonus',
      title,
      message,
    });

    await this.sendUserPush(
      user_id,
      { type: 'signup_bonus', title, message, route },
      {
        premium_until: premiumUntil ? new Date(premiumUntil).toISOString() : null,
        months: safeMonths,
      },
    );

    return {
      status: true,
      statusCode: HttpStatus.CREATED,
      data: { id: notification.id, route },
    };
  }

  async createReferralSignupNotification(input: { referrerUserId: string; referralId: string }) {
    const payload = {
      type: 'referral_signup',
      title: 'Referral code used',
      message: 'A new user signed up with your referral code. Your reward is being checked.',
      route: '/settings/referral',
    };

    const notification = await this.insertNotificationOnce({
      user_id: input.referrerUserId,
      type: payload.type,
      title: payload.title,
      message: payload.message,
      sourceEventId: input.referralId,
    });

    if (notification.inserted) {
      await this.sendUserPush(input.referrerUserId, payload, { referral_id: input.referralId });
    }

    return {
      status: true,
      statusCode: HttpStatus.CREATED,
      data: { id: notification.id, inserted: notification.inserted, route: payload.route },
    };
  }

  private buildPremiumRewardPayload(rewardType: string, grantedDays: number) {
    const dayLabel = `${grantedDays} Premium day${grantedDays === 1 ? '' : 's'}`;

    if (rewardType === 'referral_referee_bonus') {
      return {
        type: 'referee_reward_granted',
        title: 'Referral bonus unlocked',
        message: `You received ${dayLabel} for joining with a referral code.`,
        route: '/settings/subscription',
      };
    }
    if (rewardType === 'referral_milestone') {
      return {
        type: 'referral_reward_granted',
        title: 'Referral Premium unlocked',
        message: `You earned ${dayLabel} from your referrals.`,
        route: '/settings/subscription',
      };
    }
    return {
      type: 'reward_granted',
      title: 'Premium Reward Unlocked',
      message: `You earned ${dayLabel}.`,
      route: '/settings/referral',
    };
  }

  async createPremiumRewardNotificationRecord(input: {
    client: Kysely<DB>;
    user_id: string;
    rewardType: string;
    grantedDays: number;
    sourceEventId?: string | null;
  }) {
    const payload = this.buildPremiumRewardPayload(input.rewardType, input.grantedDays);
    const notification = await this.insertNotificationOnce(
      {
        user_id: input.user_id,
        type: payload.type,
        title: payload.title,
        message: payload.message,
        sourceEventId: input.sourceEventId,
      },
      input.client,
    );

    return { ...payload, inserted: notification.inserted, id: notification.id };
  }

  async sendPremiumRewardNotificationPush(input: {
    user_id: string;
    type: string;
    title: string;
    message: string;
    route: string;
    rewardType: string;
    grantedDays: number;
    sourceEventId?: string | null;
  }) {
    await this.sendUserPush(
      input.user_id,
      { type: input.type, title: input.title, message: input.message, route: input.route },
      {
        reward_type: input.rewardType,
        granted_days: input.grantedDays,
        source_event_id: input.sourceEventId || null,
      },
    );
  }
}
