import { Injectable, HttpStatus, BadRequestException, NotFoundException } from '@nestjs/common';
import { Kysely, sql } from 'kysely';
import { DB } from '@/database/database.type';

export type CreditLedgerReason =
  | 'breed_detection_debit'
  | 'ad_reward_credit'
  | 'credit_pack_purchase'
  | 'manual_adjustment'
  | 'reconciliation_adjustment';

export type CreditReferenceType =
  | 'ai_detection'
  | 'ad_reward'
  | 'purchase'
  | 'system'
  | 'migration'
  | 'report_bonus';

export interface WalletMutationInput {
  user_id: string;
  delta: number;
  reason: CreditLedgerReason;
  referenceType: CreditReferenceType;
  idempotencyKey: string;
  referenceId?: string;
  metadata?: Record<string, unknown>;
  /** Optional Kysely transaction to enlist in an outer unit of work. */
  client?: Kysely<DB>;
}

export interface WalletBalance {
  basic_credits: number;
  additional_credits: number;
  total_credits: number;
}

@Injectable()
export class WalletService {
  constructor(private readonly db: Kysely<DB>) {}

  private balanceFromRow(row: { basic_credits?: number; additional_credits?: number } | undefined): WalletBalance {
    const basicCredits = Number(row?.basic_credits || 0);
    const additionalCredits = Number(row?.additional_credits || 0);
    return {
      basic_credits: basicCredits,
      additional_credits: additionalCredits,
      total_credits: basicCredits + additionalCredits,
    };
  }

  /** Deterministic 32-bit hash so the same idempotency key maps to the same advisory lock. */
  private normalizeHashInt(key: string): number {
    let hash = 0;
    for (let i = 0; i < key.length; i += 1) {
      hash = ((hash << 5) - hash + key.charCodeAt(i)) | 0;
    }
    return hash;
  }

  private async readBalance(client: Kysely<DB>, user_id: string) {
    return client
      .selectFrom('users')
      .select([
        sql<number>`COALESCE(basic_credits, 0)::int`.as('basic_credits'),
        sql<number>`COALESCE(additional_credits, 0)::int`.as('additional_credits'),
      ])
      .where('id', '=', user_id)
      .where('deleted_at', 'is', null)
      .executeTakeFirst();
  }

  async getUserWalletBalance(user_id: string, client: Kysely<DB> = this.db) {
    const row = await this.readBalance(client, user_id);
    if (!row) {
      throw new NotFoundException('User not found');
    }
    return { status: true, statusCode: HttpStatus.OK, data: this.balanceFromRow(row) };
  }

  /**
   * Idempotent credit mutation. Positive deltas add to additional_credits;
   * negative deltas drain basic first, then additional. Guarded by a per-key
   * advisory lock + ledger uniqueness so retries can't double-apply.
   */
  async applyCreditMutation(input: WalletMutationInput) {
    const { user_id, delta, reason, referenceType, referenceId, idempotencyKey, metadata = {} } = input;

    if (!user_id || !idempotencyKey || !reason || !referenceType || !Number.isInteger(delta) || delta === 0) {
      throw new BadRequestException('Invalid wallet mutation payload');
    }

    const run = async (trx: Kysely<DB>) => {
      await sql`SELECT pg_advisory_xact_lock(${this.normalizeHashInt(idempotencyKey)})`.execute(trx);

      const existingLedger = await trx
        .selectFrom('credit_ledger')
        .select(['id', 'delta', 'balance_after', 'created_at'])
        .where('idempotency_key', '=', idempotencyKey)
        .where('deleted_at', 'is', null)
        .executeTakeFirst();

      if (existingLedger) {
        const current = await this.getUserWalletBalance(user_id, trx);
        return {
          status: true,
          statusCode: HttpStatus.OK,
          data: {
            duplicate: true,
            delta: Number(existingLedger.delta || 0),
            balance_after: Number(existingLedger.balance_after || 0),
            balance: current.data,
          },
        };
      }

      const existing = await trx
        .selectFrom('users')
        .select([
          sql<number>`COALESCE(basic_credits, 0)::int`.as('basic_credits'),
          sql<number>`COALESCE(additional_credits, 0)::int`.as('additional_credits'),
        ])
        .where('id', '=', user_id)
        .where('deleted_at', 'is', null)
        .modifyEnd(sql`FOR UPDATE`)
        .executeTakeFirst();

      if (!existing) {
        throw new NotFoundException('User not found');
      }

      const currentBalance = this.balanceFromRow(existing);

      if (delta < 0 && currentBalance.total_credits < Math.abs(delta)) {
        return {
          status: false,
          statusCode: HttpStatus.BAD_REQUEST,
          message: 'Not enough credits. Please purchase more credits.',
          data: { required_credits: Math.abs(delta), ...currentBalance },
        };
      }

      let nextBasic = currentBalance.basic_credits;
      let nextAdditional = currentBalance.additional_credits;

      if (delta > 0) {
        nextAdditional += delta;
      } else {
        const toDeduct = Math.abs(delta);
        const deductFromBasic = Math.min(nextBasic, toDeduct);
        const deductFromAdditional = Math.min(nextAdditional, toDeduct - deductFromBasic);
        nextBasic -= deductFromBasic;
        nextAdditional -= deductFromAdditional;
      }

      const totalCredits = nextBasic + nextAdditional;

      await trx
        .updateTable('users')
        .set({ basic_credits: nextBasic, additional_credits: nextAdditional, updated_at: sql`NOW()` })
        .where('id', '=', user_id)
        .where('deleted_at', 'is', null)
        .execute();

      await trx
        .insertInto('credit_ledger')
        .values({
          user_id,
          delta,
          balance_after: totalCredits,
          reason,
          reference_type: referenceType,
          reference_id: referenceId || null,
          idempotency_key: idempotencyKey,
          metadata: JSON.stringify(metadata || {}),
        })
        .execute();

      return {
        status: true,
        statusCode: HttpStatus.OK,
        data: {
          duplicate: false,
          delta,
          balance: { basic_credits: nextBasic, additional_credits: nextAdditional, total_credits: totalCredits },
        },
      };
    };

    // Enlist in an outer transaction when provided, otherwise manage our own.
    if (input.client) {
      return run(input.client);
    }
    return this.db.transaction().execute(run);
  }

  async getCreditActivity(user_id: string, page = 0, limit = 50) {
    const safePage = Math.max(0, Number(page) || 0);
    const safeLimit = Math.min(100, Math.max(1, Number(limit) || 50));
    const offset = safePage * safeLimit;

    const [rows, countResult] = await Promise.all([
      this.db
        .selectFrom('credit_ledger')
        .select([
          'id',
          'delta',
          'balance_after',
          'reason',
          'reference_type',
          'reference_id',
          'metadata',
          'created_at',
        ])
        .where('user_id', '=', user_id)
        .where('deleted_at', 'is', null)
        .orderBy('created_at', 'desc')
        .limit(safeLimit)
        .offset(offset)
        .execute(),
      this.db
        .selectFrom('credit_ledger')
        .select(sql<number>`COUNT(*)::int`.as('count'))
        .where('user_id', '=', user_id)
        .where('deleted_at', 'is', null)
        .executeTakeFirst(),
    ]);

    const total = Number(countResult?.count || 0);

    return {
      status: true,
      statusCode: HttpStatus.OK,
      data: {
        activities: rows,
        total,
        page: safePage,
        hasMore: offset + safeLimit < total,
      },
    };
  }
}
