import { Injectable } from '@nestjs/common';
import { Kysely, sql } from 'kysely';
import { DB } from '@/database/database.type';
import { Logger } from 'pino-nestjs';
import { NotificationsService } from '../notifications/notifications.service';

const MAX_REWARD_DAYS_PER_MONTH = 60;

export type GrantRewardInput = {
  userId: string;
  rewardKey: string;
  rewardType: string;
  days: number;
  reason: string;
  metadata?: Record<string, unknown>;
  sourceEventId?: string | null;
};

export type GrantRewardResult = {
  granted: boolean;
  duplicate: boolean;
  capped: boolean;
  requestedDays: number;
  grantedDays: number;
  premiumUntil: string | null;
};

function getUtcMonthRange(now: Date) {
  const start = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1));
  const end = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1));
  return { start, end };
}

export function extendPremiumFromLatest(currentExpiry: Date | null, days: number): Date {
  const now = new Date();
  const base = currentExpiry && currentExpiry.getTime() > now.getTime() ? currentExpiry : now;
  return new Date(base.getTime() + days * 24 * 60 * 60 * 1000);
}

@Injectable()
export class RewardEngineService {
  constructor(
    private readonly db: Kysely<DB>,
    private readonly logger: Logger,
    private readonly notificationsService: NotificationsService,
  ) {}

  /**
   * Idempotent (by `reward_key`) Premium-day grant with a 60-day/month cap.
   * Extends `users.premium_until`, writes ledger rows, and fires a reward
   * notification on success. Mirrors the Express reward engine.
   */
  async grantReward(input: GrantRewardInput): Promise<GrantRewardResult> {
    const requestedDays = Math.max(0, Math.floor(Number(input.days || 0)));
    if (!requestedDays) {
      return {
        granted: false,
        duplicate: false,
        capped: true,
        requestedDays: 0,
        grantedDays: 0,
        premiumUntil: null,
      };
    }

    let pendingPushNotification: Awaited<
      ReturnType<NotificationsService['createPremiumRewardNotificationRecord']>
    > | null = null;

    try {
      const result = await this.db.transaction().execute(async (trx) => {
        const existing = await sql<{ granted_days: number; status: string }>`
          SELECT granted_days, status FROM reward_grants
          WHERE reward_key = ${input.rewardKey} AND deleted_at IS NULL LIMIT 1
        `.execute(trx);

        if (existing.rows.length > 0) {
          return {
            granted: existing.rows[0].status === 'GRANTED',
            duplicate: true,
            capped: existing.rows[0].status === 'CAPPED',
            requestedDays,
            grantedDays: Number(existing.rows[0].granted_days || 0),
            premiumUntil: null,
          };
        }

        const now = new Date();
        const { start, end } = getUtcMonthRange(now);
        const monthly = await sql<{ total: number }>`
          SELECT COALESCE(SUM(granted_days), 0)::int AS total FROM reward_grants
          WHERE user_id = ${input.userId} AND status = 'GRANTED'
            AND granted_at >= ${start.toISOString()} AND granted_at < ${end.toISOString()}
            AND deleted_at IS NULL
        `.execute(trx);
        const grantedThisMonth = Number(monthly.rows[0]?.total || 0);
        const remainingCap = Math.max(0, MAX_REWARD_DAYS_PER_MONTH - grantedThisMonth);
        const grantedDays = Math.max(0, Math.min(requestedDays, remainingCap));
        const status = grantedDays > 0 ? 'GRANTED' : 'CAPPED';

        const userResult = await sql<{ premium_until: string | null }>`
          SELECT premium_until FROM users WHERE id = ${input.userId} AND deleted_at IS NULL FOR UPDATE
        `.execute(trx);
        const currentPremium = userResult.rows[0]?.premium_until
          ? new Date(userResult.rows[0].premium_until)
          : null;

        let nextPremiumIso: string | null = currentPremium ? currentPremium.toISOString() : null;
        if (grantedDays > 0) {
          const nextPremium = extendPremiumFromLatest(currentPremium, grantedDays);
          nextPremiumIso = nextPremium.toISOString();

          await sql`
            UPDATE users SET premium_until = ${nextPremiumIso}, premium_source = ${input.rewardType}, updated_at = NOW()
            WHERE id = ${input.userId} AND deleted_at IS NULL
          `.execute(trx);

          await sql`
            INSERT INTO referral_rewards (user_id, type, days_awarded, reason)
            VALUES (${input.userId}, ${input.rewardType}, ${grantedDays}, ${input.reason})
          `.execute(trx);

          pendingPushNotification =
            await this.notificationsService.createPremiumRewardNotificationRecord({
              client: trx,
              user_id: input.userId,
              rewardType: input.rewardType,
              grantedDays,
              sourceEventId: input.sourceEventId || input.rewardKey,
            });
        }

        await sql`
          INSERT INTO reward_grants
            (user_id, reward_key, reward_type, requested_days, granted_days, status, reason, metadata, source_event_id, granted_at, created_at, updated_at)
          VALUES (${input.userId}, ${input.rewardKey}, ${input.rewardType}, ${requestedDays}, ${grantedDays}, ${status}, ${input.reason}, ${JSON.stringify(input.metadata || {})}, ${input.sourceEventId || null}, NOW(), NOW(), NOW())
        `.execute(trx);

        return {
          granted: grantedDays > 0,
          duplicate: false,
          capped: grantedDays === 0,
          requestedDays,
          grantedDays,
          premiumUntil: nextPremiumIso,
        };
      });

      if (pendingPushNotification && (pendingPushNotification as any).inserted) {
        const note = pendingPushNotification as any;
        this.notificationsService
          .sendPremiumRewardNotificationPush({
            user_id: input.userId,
            type: note.type,
            title: note.title,
            message: note.message,
            route: note.route,
            rewardType: input.rewardType,
            grantedDays: result.grantedDays,
            sourceEventId: input.sourceEventId || input.rewardKey,
          })
          .catch((error) => this.logger.error('Failed to send premium reward notification push', error));
      }

      return result;
    } catch (error: any) {
      if (error?.code === '23505') {
        const duplicate = await sql<{ granted_days: number; status: string }>`
          SELECT granted_days, status FROM reward_grants
          WHERE reward_key = ${input.rewardKey} AND deleted_at IS NULL LIMIT 1
        `.execute(this.db);
        return {
          granted: duplicate.rows[0]?.status === 'GRANTED',
          duplicate: true,
          capped: duplicate.rows[0]?.status === 'CAPPED',
          requestedDays,
          grantedDays: Number(duplicate.rows[0]?.granted_days || 0),
          premiumUntil: null,
        };
      }
      throw error;
    }
  }
}
