import {
  BadRequestException,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import { PrismaService } from '../../prisma/prisma.service';
import { ConnectAccountDto } from './dto/connect-account.dto';

type LinkedAccountEntity = {
  id: number;
  userId: number;
  type: string;
  provider: string;
  providerEn: string | null;
  accountNumber: string | null;
  alias: string | null;
  aliasEn: string | null;
  status: string;
  autoImport: boolean;
  lastSyncAt: Date | null;
  transactionCount: number;
  monthlyImported: number;
  createdAt: Date;
  updatedAt: Date;
};

@Injectable()
export class IntegrationsService {
  constructor(private readonly prisma: PrismaService) {}

  private serializeBigInt(data: any): any {
    return JSON.parse(
      JSON.stringify(data, (_, v) => (typeof v === 'bigint' ? Number(v) : v)),
    );
  }

  async getAccounts(userId: number) {
    const accounts = await this.prisma.linkedAccount.findMany({
      where: { userId },
      orderBy: { createdAt: 'desc' },
    });

    return this.serializeBigInt(accounts);
  }

  async connectAccount(userId: number, dto: ConnectAccountDto) {
    const normalizedType = dto.type?.toLowerCase();
    if (!['card', 'bank'].includes(normalizedType)) {
      throw new BadRequestException('Account type must be card or bank');
    }

    await this.ensureSlotAvailable(userId, normalizedType);

    const account = await this.prisma.linkedAccount.create({
      data: {
        ...dto,
        type: normalizedType,
        accountNumber:
          dto.accountNumber || (await this.generateMaskedAccountNumber(userId, normalizedType)),
        userId,
        status: 'connected',
      },
    });

    return this.serializeBigInt(account);
  }

  async disconnectAccount(userId: number, id: number) {
    const existing = await this.prisma.linkedAccount.findFirst({
      where: { id, userId },
    });

    if (!existing) {
      throw new NotFoundException(`Linked account with ID ${id} not found`);
    }

    await this.prisma.linkedAccount.delete({ where: { id } });

    return { message: `Linked account with ID ${id} has been disconnected` };
  }

  async syncAccount(userId: number, id: number) {
    const existing = await this.prisma.linkedAccount.findFirst({
      where: { id, userId },
    });

    if (!existing) {
      throw new NotFoundException(`Linked account with ID ${id} not found`);
    }

    if (!existing.autoImport) {
      throw new BadRequestException(
        'Enable auto import before syncing this account',
      );
    }

    const importedCount = await this.importTransactions(userId, existing);
    const monthlyImported = await this.countMonthlyImports(existing.id);

    const updated = await this.prisma.linkedAccount.update({
      where: { id },
      data: {
        lastSyncAt: new Date(),
        transactionCount: existing.transactionCount + importedCount,
        monthlyImported,
      },
    });

    return this.serializeBigInt({
      ...updated,
      importedCount,
    });
  }

  async toggleAutoImport(userId: number, id: number) {
    const existing = await this.prisma.linkedAccount.findFirst({
      where: { id, userId },
    });

    if (!existing) {
      throw new NotFoundException(`Linked account with ID ${id} not found`);
    }

    const updated = await this.prisma.linkedAccount.update({
      where: { id },
      data: { autoImport: !existing.autoImport },
    });

    return this.serializeBigInt(updated);
  }

  private async ensureSlotAvailable(userId: number, type: string) {
    const addonId = type === 'card' ? 'card-sync' : 'bank-sync';

    const [slotSubscription, usedSlots] = await Promise.all([
      this.prisma.addonSubscription.findFirst({
        where: {
          userId,
          isActive: true,
          addon: {
            addonId,
            isActive: true,
          },
        },
        include: {
          addon: true,
        },
      }),
      this.prisma.linkedAccount.count({
        where: {
          userId,
          type,
        },
      }),
    ]);

    const limit = slotSubscription?.quantity || 0;
    if (usedSlots >= limit) {
      throw new BadRequestException(
        `${type} sync slot limit reached. Subscribe to more ${addonId} slots first.`,
      );
    }
  }

  private async generateMaskedAccountNumber(userId: number, type: string) {
    const count = await this.prisma.linkedAccount.count({
      where: { userId, type },
    });
    const suffix = String((userId * 37 + count + 1) % 10000).padStart(4, '0');
    return type === 'card'
      ? `****-****-****-${suffix}`
      : `***-**-****-${suffix}`;
  }

  private async importTransactions(
    userId: number,
    account: LinkedAccountEntity,
  ) {
    const catalog = this.getMerchantCatalog(account.type);
    const today = new Date();
    const startDate = account.lastSyncAt
      ? new Date(account.lastSyncAt)
      : new Date(today.getFullYear(), today.getMonth(), Math.max(today.getDate() - 6, 1));
    const importDates = this.buildImportDates(startDate, today);
    const categories = await this.prisma.expenseCategory.findMany({
      where: { userId },
    });

    let importedCount = 0;

    for (let index = 0; index < importDates.length; index += 1) {
      const merchant = catalog[index % catalog.length];
      const transactionDate = importDates[index];
      const amount = merchant.baseAmount + this.buildAmountOffset(account.id, index);
      const itemName = `${account.provider} ${merchant.nameKo}`;

      const existingExpense = await this.prisma.expense.findFirst({
        where: {
          userId,
          linkedAccountId: account.id,
          autoImported: true,
          expenseDate: transactionDate,
          item: itemName,
          amount: BigInt(amount),
        },
      });

      if (existingExpense) {
        continue;
      }

      const matchedCategory = categories.find(
        (category) => category.slug === merchant.categorySlug,
      );

      await this.prisma.expense.create({
        data: {
          userId,
          categoryId: matchedCategory?.id,
          expenseDate: transactionDate,
          item: itemName,
          itemEn: `${account.providerEn || account.provider} ${merchant.nameEn}`,
          amount: BigInt(amount),
          paymentMethod: account.type === 'card' ? 'business-card' : 'bank-transfer',
          paymentMethodEn:
            account.type === 'card' ? 'Business Card' : 'Bank Transfer',
          proof: account.type === 'card' ? 'card-receipt' : 'bank-transfer-slip',
          proofEn:
            account.type === 'card' ? 'Card Receipt' : 'Bank Transfer Slip',
          autoImported: true,
          status: 'confirmed',
          linkedAccountId: account.id,
          note: `${account.alias || account.provider} 자동 수집`,
        },
      });
      importedCount += 1;
    }

    return importedCount;
  }

  private buildImportDates(startDate: Date, endDate: Date) {
    const dates: Date[] = [];
    const cursor = new Date(
      startDate.getFullYear(),
      startDate.getMonth(),
      startDate.getDate(),
    );
    cursor.setDate(cursor.getDate() + 1);

    while (cursor <= endDate && dates.length < 6) {
      if (cursor.getDay() !== 0) {
        dates.push(new Date(cursor));
      }
      cursor.setDate(cursor.getDate() + 2);
    }

    if (dates.length === 0) {
      dates.push(new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate()));
    }

    return dates;
  }

  private buildAmountOffset(accountId: number, index: number) {
    return ((accountId + 1) * (index + 3) * 137) % 9000;
  }

  private getMerchantCatalog(type: string) {
    if (type === 'bank') {
      return [
        {
          nameKo: '클라우드 사용료',
          nameEn: 'Cloud Infrastructure',
          categorySlug: 'infrastructure',
          baseAmount: 48000,
        },
        {
          nameKo: '외주 정산',
          nameEn: 'Outsourcing Payment',
          categorySlug: 'outsourcing',
          baseAmount: 220000,
        },
        {
          nameKo: '온라인 광고비',
          nameEn: 'Digital Marketing',
          categorySlug: 'marketing',
          baseAmount: 91000,
        },
      ];
    }

    return [
      {
        nameKo: 'SaaS 구독',
        nameEn: 'SaaS Subscription',
        categorySlug: 'saas',
        baseAmount: 29000,
      },
      {
        nameKo: '소프트웨어 결제',
        nameEn: 'Software Purchase',
        categorySlug: 'software',
        baseAmount: 54000,
      },
      {
        nameKo: '교통비',
        nameEn: 'Transportation',
        categorySlug: 'transport',
        baseAmount: 17000,
      },
    ];
  }

  private async countMonthlyImports(linkedAccountId: number) {
    const now = new Date();
    const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
    const monthEnd = new Date(
      now.getFullYear(),
      now.getMonth() + 1,
      0,
      23,
      59,
      59,
      999,
    );

    return this.prisma.expense.count({
      where: {
        linkedAccountId,
        autoImported: true,
        expenseDate: {
          gte: monthStart,
          lte: monthEnd,
        },
      },
    });
  }
}
