import {
  Injectable,
  ConflictException,
  BadRequestException,
  InternalServerErrorException,
  ServiceUnavailableException,
} from '@nestjs/common';
import { Kysely, sql } from 'kysely';
import { createHash, randomUUID } from 'crypto';
import { DB } from '@/database/database.type';
import { Logger } from 'pino-nestjs';
import { UsersService } from '../users/users.service';
import { BillingPlatform, PurchaseEventType, SubscriptionStatus, VerifyPurchaseInput } from './billing.types';
import { verifyApplePurchase } from './verifyApple';
import { verifyGooglePurchase } from './verifyGoogle';
import { getActiveExternalPremium, upsertSubscription } from './subscriptionEngine';
import { logPurchaseEvent } from './purchaseLogger';
import { BILLING_PRODUCTS, getCreditPackage, isCreditProduct, isSubscriptionProduct } from './billingProducts';

type WebhookSource = 'apple' | 'google';

function parseTimestamp(input: unknown): Date | null {
  if (input === undefined || input === null || input === '') return null;
  const numeric = Number(input);
  if (Number.isFinite(numeric) && numeric > 0) {
    return new Date(numeric);
  }
  const parsed = Date.parse(String(input));
  if (Number.isFinite(parsed)) {
    return new Date(parsed);
  }
  return null;
}

function platformFromSource(source: WebhookSource): BillingPlatform {
  return source === 'apple' ? 'ios' : 'android';
}

function toEventType(source: WebhookSource, rawType: string): PurchaseEventType {
  const normalized = rawType.toUpperCase();

  if (source === 'apple') {
    if (normalized.includes('DID_RENEW')) return 'subscription_renewed';
    if (normalized.includes('EXPIRED')) return 'subscription_expired';
    if (normalized.includes('REFUND')) return 'subscription_refunded';
    if (normalized.includes('REVOKE')) return 'subscription_revoked';
    if (normalized.includes('CANCEL')) return 'subscription_cancelled';
    return 'purchase_created';
  }

  if (normalized.includes('RENEW')) return 'subscription_renewed';
  if (normalized.includes('EXPIRE')) return 'subscription_expired';
  if (normalized.includes('CANCEL')) return 'subscription_cancelled';
  if (normalized.includes('REFUND')) return 'subscription_refunded';
  if (normalized.includes('REVOKE')) return 'subscription_revoked';
  return 'purchase_created';
}

function isDbTimeoutError(error: unknown): boolean {
  const message = String((error as any)?.message || '').toLowerCase();
  return message.includes('timeout') || message.includes('connection terminated');
}

function appleStatusCodeMeaning(code: unknown): string | null {
  const value = Number(code);
  if (!Number.isFinite(value)) return null;
  switch (value) {
    case 1:
      return 'active';
    case 2:
      return 'expired';
    case 3:
      return 'billing_retry';
    case 4:
      return 'billing_grace_period';
    case 5:
      return 'revoked';
    default:
      return 'unknown';
  }
}

function toSubscriptionStatus(eventType: PurchaseEventType): SubscriptionStatus {
  if (eventType === 'subscription_expired') return 'expired';
  if (eventType === 'subscription_cancelled') return 'cancelled';
  if (eventType === 'subscription_revoked' || eventType === 'subscription_refunded') return 'revoked';
  return 'active';
}

const SIGNUP_BONUS_MONTHS_SETTING_KEY = 'signup_bonus_months';
const SIGNUP_BONUS_EVENT_TYPE: PurchaseEventType = 'singup_bonus';

function parsePositiveInteger(input: unknown, fallback: number): number {
  const parsed = Number.parseInt(String(input ?? ''), 10);
  if (!Number.isFinite(parsed) || parsed < 0) return fallback;
  return parsed;
}

function addCalendarMonths(date: Date, months: number): Date {
  const next = new Date(date.getTime());
  next.setMonth(next.getMonth() + months);
  return next;
}

function eventIdFromPayload(source: WebhookSource, payload: Record<string, unknown>): string {
  const direct =
    (payload.event_id as string) ||
    (payload.notificationId as string) ||
    (payload.id as string) ||
    (payload.orderId as string);

  if (direct) return String(direct);

  const hash = createHash('sha256').update(source).update(JSON.stringify(payload)).digest('hex');

  return `derived_${hash}`;
}

@Injectable()
export class BillingService {
  constructor(
    private readonly db: Kysely<DB>,
    private readonly logger: Logger,
    private readonly usersService: UsersService,
  ) {}

  /**
   * One-time free-Premium grant on signup. Idempotent per user via a
   * `purchase_events` row of type `SIGNUP_BONUS_EVENT_TYPE` — mirrors the
   * Express `grantSignupBonusPremium` implementation.
   */
  async grantSignupBonusPremium(options: { user_id: string; platform?: BillingPlatform }) {
    return this.db.transaction().execute(async (trx) => {
      const existing = await trx
        .selectFrom('purchase_events')
        .select(['product_id', 'expires_at'])
        .where('user_id', '=', options.user_id)
        .where('event_type', '=', SIGNUP_BONUS_EVENT_TYPE)
        .where('deleted_at', 'is', null)
        .orderBy('created_at', 'asc')
        .executeTakeFirst();

      if (existing) {
        return {
          granted: false,
          premiumUntil: existing.expires_at || null,
          productId: existing.product_id || null,
          months: null as number | null,
        };
      }

      const setting = await trx
        .selectFrom('app_settings')
        .select('value')
        .where('key', '=', SIGNUP_BONUS_MONTHS_SETTING_KEY)
        .where('deleted_at', 'is', null)
        .executeTakeFirst();
      const months = parsePositiveInteger(setting?.value, 1);

      if (months <= 0) {
        return { granted: false, premiumUntil: null, productId: null, months };
      }

      const productId = `${months}_months`;
      const expiresAt = addCalendarMonths(new Date(), months);
      const transactionId = `signup_bonus_${options.user_id}`;

      await upsertSubscription({
        db: trx,
        user_id: options.user_id,
        platform: options.platform || 'ios',
        sourceOverride: 'signup_bonus',
        productId,
        status: 'active',
        transactionId,
        originalTransactionId: transactionId,
        expiresAt,
      });

      await logPurchaseEvent({
        db: trx,
        user_id: options.user_id,
        platform: options.platform || 'ios',
        eventType: SIGNUP_BONUS_EVENT_TYPE,
        productId,
        transactionId,
        originalTransactionId: transactionId,
        expiresAt,
        storePayload: {
          source: 'signup_bonus',
          months,
          settingKey: SIGNUP_BONUS_MONTHS_SETTING_KEY,
          reason: 'New user signup bonus',
        },
      });

      return { granted: true, premiumUntil: expiresAt, productId, months };
    });
  }

  async verifyPurchase(input: VerifyPurchaseInput) {
    if (isSubscriptionProduct(input.productId)) {
      const activeExternalPremium = await getActiveExternalPremium(this.db, input.user_id);
      if (activeExternalPremium) {
        return {
          status: true,
          statusCode: 200,
          data: {
            active: true,
            status: 'active',
            source: activeExternalPremium.premiumSource,
            expiresAt: activeExternalPremium.premiumUntil.toISOString(),
            productId: input.productId,
            transactionId: null,
            verificationSkipped: true,
            reason: 'active_external_premium',
          },
        };
      }
    }

    const normalized =
      input.platform === 'ios' ? await verifyApplePurchase(input) : await verifyGooglePurchase(input);

    const creditPackage = getCreditPackage(normalized.productId);
    const isCreditPack = isCreditProduct(normalized.productId);
    const originalTxId = normalized.originalTransactionId || normalized.transactionId;

    return await this.db.transaction().execute(async (trx) => {
      if (!isCreditPack && originalTxId) {
        const boundResult = await trx
          .selectFrom('subscriptions')
          .select('user_id')
          .where('original_transaction_id', '=', originalTxId)
          .where('deleted_at', 'is', null)
          .orderBy('updated_at', 'desc')
          .limit(1)
          .executeTakeFirst();

        const boundUserId = boundResult?.user_id ? String(boundResult.user_id) : '';

        if (boundUserId && boundUserId !== normalized.user_id) {
          if (!input.forceTransfer) {
            throw new ConflictException({
              status: false,
              code: 'PURCHASE_BOUND_TO_ANOTHER_ACCOUNT',
              message: 'This store subscription is already linked to another Fur Finder account.',
              data: {
                boundUserId,
                originalTransactionId: originalTxId,
              },
            });
          }

          await trx
            .updateTable('subscriptions')
            .set({
              user_id: normalized.user_id,
              updated_at: new Date(),
            })
            .where('original_transaction_id', '=', originalTxId)
            .where('deleted_at', 'is', null)
            .execute();

          await trx
            .updateTable('purchase_events')
            .set({
              user_id: normalized.user_id,
            })
            .where('original_transaction_id', '=', originalTxId)
            .where('deleted_at', 'is', null)
            .execute();
        }
      }

      const payloadReasonRaw =
        String((input.payload as any)?.transactionReasonIOS || '') ||
        String((input.payload as any)?.reasonIOS || '') ||
        String((input.payload as any)?.reason || '');
      const isRenewal = payloadReasonRaw.toUpperCase().includes('RENEW');
      const eventType: PurchaseEventType = isRenewal ? 'subscription_renewed' : 'purchase_created';

      if (!isCreditPack) {
        await upsertSubscription({
          db: trx,
          user_id: normalized.user_id,
          platform: normalized.platform,
          productId: normalized.productId,
          status: normalized.status,
          transactionId: normalized.transactionId,
          originalTransactionId: normalized.originalTransactionId,
          expiresAt: normalized.expiresAt,
        });
      }

      await logPurchaseEvent({
        db: trx,
        user_id: normalized.user_id,
        platform: normalized.platform,
        eventType,
        productId: normalized.productId,
        transactionId: normalized.transactionId,
        originalTransactionId: normalized.originalTransactionId,
        expiresAt: normalized.expiresAt,
        storePayload: normalized.rawPayload,
      });

      let creditGrantResult: any = null;
      if (isCreditPack && creditPackage) {
        creditGrantResult = await this.usersService.grantAdditionalCreditsForPurchase({
          client: trx,
          user_id: normalized.user_id,
          productId: normalized.productId,
          platform: normalized.platform,
          credits: creditPackage.credits,
          priceUsd: creditPackage.priceUsd,
          transactionId: normalized.transactionId,
          originalTransactionId: normalized.originalTransactionId,
          purchaseToken: input.purchaseToken,
          storePayload: normalized.rawPayload,
        });
      }

      if (isCreditPack) {
        return {
          status: true,
          statusCode: 200,
          data: {
            active: true,
            creditPurchase: true,
            productId: normalized.productId,
            transactionId: normalized.transactionId,
            creditsAdded: creditGrantResult?.data?.credits_added || 0,
            duplicate: !!creditGrantResult?.data?.duplicate,
            balance: creditGrantResult?.data?.balance || null,
          },
        };
      }

      const diagnostics =
        normalized.platform === 'ios'
          ? {
              environment: (normalized.rawPayload as any)?.environment || null,
              subscriptionStatusCode: (normalized.rawPayload as any)?.subscriptionStatusCode ?? null,
              subscriptionStatusMeaning: appleStatusCodeMeaning(
                (normalized.rawPayload as any)?.subscriptionStatusCode,
              ),
              transactionExpiresDateMs: (normalized.rawPayload as any)?.transactionPayload?.expiresDate ?? null,
            }
          : null;

      return {
        status: true,
        statusCode: 200,
        data: {
          active: normalized.status === 'active',
          status: normalized.status,
          source: normalized.platform === 'ios' ? 'apple' : 'google',
          diagnostics,
          expiresAt: normalized.expiresAt || null,
          productId: normalized.productId,
          transactionId: normalized.transactionId,
        },
      };
    });
  }

  async getUserSubscription(userId: string) {
    try {
      const row = await this.db
        .selectFrom('subscriptions')
        .select([
          'id',
          'user_id',
          'plan',
          'status',
          'source',
          'product_id',
          'current_period_start',
          'current_period_end',
          'original_transaction_id',
          'latest_transaction_id',
          'created_at',
          'updated_at',
        ])
        .where('user_id', '=', userId)
        .where('deleted_at', 'is', null)
        .orderBy('updated_at', 'desc')
        .limit(1)
        .executeTakeFirst();

      return {
        status: true,
        statusCode: 200,
        data: row || null,
      };
    } catch (error) {
      this.logger.error('Failed to get user subscription', error);
      if (isDbTimeoutError(error)) {
        throw new ServiceUnavailableException('Billing is temporarily unavailable. Please try again.');
      }
      throw new InternalServerErrorException('Failed to retrieve user subscription');
    }
  }

  async getPurchaseHistory(userId: string) {
    try {
      const rows = await this.db
        .selectFrom('purchase_events')
        .select([
          'id',
          'user_id',
          'platform',
          'event_type',
          'transaction_id',
          'original_transaction_id',
          'product_id',
          'expires_at',
          'store_payload',
          'created_at',
        ])
        .where('user_id', '=', userId)
        .where('deleted_at', 'is', null)
        .orderBy('created_at', 'desc')
        .execute();

      return {
        status: true,
        statusCode: 200,
        data: rows,
      };
    } catch (error) {
      this.logger.error('Failed to get purchase history', error);
      if (isDbTimeoutError(error)) {
        throw new ServiceUnavailableException('Billing history is temporarily unavailable. Please try again.');
      }
      throw new InternalServerErrorException('Failed to retrieve purchase history');
    }
  }

  async processWebhook(options: { source: WebhookSource; payload: Record<string, unknown> }) {
    const { source, payload } = options;
    const eventId = eventIdFromPayload(source, payload);
    const user_id = String(payload.user_id || payload.app_user_id || '');
    const productId = String(payload.product_id || payload.productId || '');
    const rawType = String(payload.event_type || payload.type || payload.notificationType || 'UNKNOWN');
    const transactionIdValue = payload.transaction_id || payload.transactionId || payload.purchaseToken;
    const transactionId = transactionIdValue ? String(transactionIdValue) : '';
    const originalTransactionId = String(
      payload.original_transaction_id || payload.originalTransactionId || transactionId,
    );
    const expiresAtRaw = payload.expires_at || payload.expiresAt || payload.expirationDate;
    const expiresAt = parseTimestamp(expiresAtRaw) || new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);

    if (!user_id || !productId || !transactionId) {
      return {
        status: true,
        statusCode: 200,
        data: { processed: false, reason: 'missing_user_or_product_or_transaction' },
      };
    }

    const eventType = toEventType(source, rawType);
    const status = toSubscriptionStatus(eventType);
    const isCreditPack = isCreditProduct(productId);

    try {
      return await this.db.transaction().execute(async (trx) => {
        const inserted = await trx
          .insertInto('webhook_events')
          .values({
            source,
            event_id: eventId,
            payload: JSON.stringify(payload) as any,
            processed: false,
          })
          .onConflict((oc) => oc.columns(['source', 'event_id']).doNothing())
          .returning('id')
          .executeTakeFirst();

        if (!inserted) {
          return {
            status: true,
            statusCode: 200,
            data: { processed: false, duplicate: true },
          };
        }

        if (!isCreditPack) {
          await upsertSubscription({
            db: trx,
            user_id,
            platform: platformFromSource(source),
            productId,
            status,
            transactionId,
            originalTransactionId,
            expiresAt,
          });
        }

        await logPurchaseEvent({
          db: trx,
          user_id,
          platform: platformFromSource(source),
          eventType,
          productId,
          transactionId,
          originalTransactionId,
          expiresAt,
          storePayload: payload,
        });

        if (isCreditPack) {
          const creditPackage = getCreditPackage(productId);
          if (creditPackage) {
            await this.usersService.grantAdditionalCreditsForPurchase({
              client: trx,
              user_id,
              productId,
              platform: platformFromSource(source),
              credits: creditPackage.credits,
              priceUsd: creditPackage.priceUsd,
              transactionId,
              originalTransactionId,
              purchaseToken: String(payload.purchaseToken || ''),
              storePayload: payload,
            });
          }
        }

        await trx
          .updateTable('webhook_events')
          .set({
            processed: true,
            processed_at: new Date(),
          })
          .where('source', '=', source)
          .where('event_id', '=', eventId)
          .execute();

        return {
          status: true,
          statusCode: 200,
          data: { processed: true, eventId },
        };
      });
    } catch (error) {
      this.logger.error('Webhook processing failed', error);
      throw new InternalServerErrorException('Webhook processing failed');
    }
  }

  async grantPremium(options: {
    user_id: string;
    days?: number;
    platform?: BillingPlatform;
    source: 'manual';
    eventType: 'manual_grant';
    metadata?: Record<string, unknown>;
  }) {
    const expiresAt = new Date(Date.now() + (options.days || 30) * 24 * 60 * 60 * 1000);

    try {
      return await this.db.transaction().execute(async (trx) => {
        await upsertSubscription({
          db: trx,
          user_id: options.user_id,
          platform: options.platform || 'ios',
          sourceOverride: 'manual',
          productId: BILLING_PRODUCTS.PREMIUM_MONTHLY,
          status: 'active',
          transactionId: `manual_${randomUUID()}`,
          originalTransactionId: `manual_${randomUUID()}`,
          expiresAt,
        });

        await logPurchaseEvent({
          db: trx,
          user_id: options.user_id,
          platform: options.platform || 'ios',
          eventType: options.eventType,
          productId: BILLING_PRODUCTS.PREMIUM_MONTHLY,
          expiresAt,
          storePayload: options.metadata || {},
        });

        return { status: true, statusCode: 200, premiumUntil: expiresAt };
      });
    } catch (error) {
      this.logger.error('Failed to grant manual premium', error);
      throw new InternalServerErrorException('Failed to grant premium access');
    }
  }
}
