import {
  Injectable,
  NotFoundException,
  BadRequestException,
} from '@nestjs/common';
import { PrismaService } from '../../prisma/prisma.service';
import { CreateExpenseDto } from './dto/create-expense.dto';
import { UpdateExpenseDto } from './dto/update-expense.dto';
import { CreateFixedExpenseDto } from './dto/create-fixed-expense.dto';
import { UpdateFixedExpenseDto } from './dto/update-fixed-expense.dto';
import { CreateCategoryDto } from './dto/create-category.dto';
import { PaginationDto } from '../../common/dto/pagination.dto';

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

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

  // ========================
  // Expenses CRUD
  // ========================

  async findAll(
    userId: number,
    query: PaginationDto & {
      categoryId?: string;
      startDate?: string;
      endDate?: string;
    },
  ) {
    const {
      page = 1,
      limit = 20,
      search,
      sortBy,
      sortOrder,
      categoryId,
      startDate,
      endDate,
    } = query;
    const skip = (page - 1) * limit;

    const where: any = { userId };

    if (categoryId) {
      where.categoryId = Number(categoryId);
    }

    if (startDate || endDate) {
      where.expenseDate = {};
      if (startDate) {
        where.expenseDate.gte = new Date(startDate);
      }
      if (endDate) {
        where.expenseDate.lte = new Date(endDate);
      }
    }

    if (search) {
      where.OR = [
        { item: { contains: search } },
        { itemEn: { contains: search } },
        { note: { contains: search } },
      ];
    }

    const orderBy: any = {};
    if (sortBy) {
      orderBy[sortBy] = sortOrder || 'asc';
    } else {
      orderBy.expenseDate = 'desc';
    }

    const [data, total] = await Promise.all([
      this.prisma.expense.findMany({
        where,
        skip,
        take: limit,
        orderBy,
        include: {
          category: true,
        },
      }),
      this.prisma.expense.count({ where }),
    ]);

    return {
      data: this.serializeBigInt(data),
      total,
      page,
      limit,
    };
  }

  async findOne(userId: number, id: number) {
    const expense = await this.prisma.expense.findFirst({
      where: { id, userId },
      include: {
        category: true,
      },
    });

    if (!expense) {
      throw new NotFoundException(`Expense with ID ${id} not found`);
    }

    return this.serializeBigInt(expense);
  }

  async create(userId: number, dto: CreateExpenseDto) {
    const data: any = {
      userId,
      expenseDate: new Date(dto.expenseDate),
      item: dto.item,
      amount: BigInt(dto.amount),
    };

    if (dto.categoryId !== undefined) data.categoryId = dto.categoryId;
    if (dto.itemEn !== undefined) data.itemEn = dto.itemEn;
    if (dto.paymentMethod !== undefined) data.paymentMethod = dto.paymentMethod;
    if (dto.paymentMethodEn !== undefined)
      data.paymentMethodEn = dto.paymentMethodEn;
    if (dto.proof !== undefined) data.proof = dto.proof;
    if (dto.proofEn !== undefined) data.proofEn = dto.proofEn;
    if (dto.autoImported !== undefined) data.autoImported = dto.autoImported;
    if (dto.status !== undefined) data.status = dto.status;
    if (dto.linkedAccountId !== undefined)
      data.linkedAccountId = dto.linkedAccountId;
    if (dto.receiptUrl !== undefined) data.receiptUrl = dto.receiptUrl;
    if (dto.note !== undefined) data.note = dto.note;

    const expense = await this.prisma.expense.create({
      data,
      include: {
        category: true,
      },
    });

    return this.serializeBigInt(expense);
  }

  async update(userId: number, id: number, dto: UpdateExpenseDto) {
    const existing = await this.prisma.expense.findFirst({
      where: { id, userId },
    });

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

    const data: any = {};

    if (dto.categoryId !== undefined) data.categoryId = dto.categoryId;
    if (dto.expenseDate !== undefined)
      data.expenseDate = new Date(dto.expenseDate);
    if (dto.item !== undefined) data.item = dto.item;
    if (dto.itemEn !== undefined) data.itemEn = dto.itemEn;
    if (dto.amount !== undefined) data.amount = BigInt(dto.amount);
    if (dto.paymentMethod !== undefined) data.paymentMethod = dto.paymentMethod;
    if (dto.paymentMethodEn !== undefined)
      data.paymentMethodEn = dto.paymentMethodEn;
    if (dto.proof !== undefined) data.proof = dto.proof;
    if (dto.proofEn !== undefined) data.proofEn = dto.proofEn;
    if (dto.autoImported !== undefined) data.autoImported = dto.autoImported;
    if (dto.status !== undefined) data.status = dto.status;
    if (dto.linkedAccountId !== undefined)
      data.linkedAccountId = dto.linkedAccountId;
    if (dto.receiptUrl !== undefined) data.receiptUrl = dto.receiptUrl;
    if (dto.note !== undefined) data.note = dto.note;

    const updated = await this.prisma.expense.update({
      where: { id },
      data,
      include: {
        category: true,
      },
    });

    return this.serializeBigInt(updated);
  }

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

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

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

    return { message: `Expense with ID ${id} has been deleted` };
  }

  // ========================
  // Categories
  // ========================

  async getCategories(userId: number) {
    const categories = await this.prisma.expenseCategory.findMany({
      where: { userId },
      orderBy: { nameKo: 'asc' },
    });

    return this.serializeBigInt(categories);
  }

  async createCategory(userId: number, dto: CreateCategoryDto) {
    const category = await this.prisma.expenseCategory.create({
      data: {
        userId,
        nameKo: dto.nameKo,
        nameEn: dto.nameEn,
        slug: dto.slug,
        isSystem: false,
      },
    });

    return this.serializeBigInt(category);
  }

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

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

    if (existing.isSystem) {
      throw new BadRequestException('System categories cannot be deleted');
    }

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

    return { message: `Category with ID ${id} has been deleted` };
  }

  // ========================
  // Fixed Expenses
  // ========================

  async getFixedExpenses(userId: number) {
    const fixedExpenses = await this.prisma.fixedExpense.findMany({
      where: { userId },
      include: {
        category: true,
      },
      orderBy: { createdAt: 'desc' },
    });

    return this.serializeBigInt(fixedExpenses);
  }

  async createFixedExpense(userId: number, dto: CreateFixedExpenseDto) {
    const data: any = {
      userId,
      name: dto.name,
      amount: BigInt(dto.amount),
    };

    if (dto.categoryId !== undefined) data.categoryId = dto.categoryId;
    if (dto.nameEn !== undefined) data.nameEn = dto.nameEn;
    if (dto.billingCycle !== undefined) data.billingCycle = dto.billingCycle;
    if (dto.nextBillingDate !== undefined)
      data.nextBillingDate = new Date(dto.nextBillingDate);

    const fixedExpense = await this.prisma.fixedExpense.create({
      data,
      include: {
        category: true,
      },
    });

    return this.serializeBigInt(fixedExpense);
  }

  async updateFixedExpense(
    userId: number,
    id: number,
    dto: UpdateFixedExpenseDto,
  ) {
    const existing = await this.prisma.fixedExpense.findFirst({
      where: { id, userId },
    });

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

    const data: any = {};

    if (dto.categoryId !== undefined) data.categoryId = dto.categoryId;
    if (dto.name !== undefined) data.name = dto.name;
    if (dto.nameEn !== undefined) data.nameEn = dto.nameEn;
    if (dto.amount !== undefined) data.amount = BigInt(dto.amount);
    if (dto.billingCycle !== undefined) data.billingCycle = dto.billingCycle;
    if (dto.nextBillingDate !== undefined)
      data.nextBillingDate = new Date(dto.nextBillingDate);

    const updated = await this.prisma.fixedExpense.update({
      where: { id },
      data,
      include: {
        category: true,
      },
    });

    return this.serializeBigInt(updated);
  }

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

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

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

    return { message: `Fixed expense with ID ${id} has been deleted` };
  }

  // ========================
  // Monthly Summary
  // ========================

  async getMonthlySummary(userId: number, year: number, month: number) {
    const startDate = new Date(year, month - 1, 1);
    const endDate = new Date(year, month, 0, 23, 59, 59, 999);

    const expenses = await this.prisma.expense.findMany({
      where: {
        userId,
        expenseDate: {
          gte: startDate,
          lte: endDate,
        },
      },
      include: {
        category: true,
      },
    });

    // Aggregate by category
    const categoryMap = new Map<
      string,
      { categoryId: number | null; nameKo: string; nameEn: string; total: bigint; count: number }
    >();

    for (const expense of expenses) {
      const key = expense.categoryId
        ? String(expense.categoryId)
        : 'uncategorized';
      const existing = categoryMap.get(key);

      if (existing) {
        existing.total += expense.amount;
        existing.count += 1;
      } else {
        categoryMap.set(key, {
          categoryId: expense.categoryId,
          nameKo: expense.category?.nameKo || '미분류',
          nameEn: expense.category?.nameEn || 'Uncategorized',
          total: expense.amount,
          count: 1,
        });
      }
    }

    const totalAmount = expenses.reduce(
      (sum, e) => sum + e.amount,
      BigInt(0),
    );

    const byCategory = Array.from(categoryMap.values()).map((item) => ({
      categoryId: item.categoryId,
      nameKo: item.nameKo,
      nameEn: item.nameEn,
      total: Number(item.total),
      count: item.count,
    }));

    return {
      year,
      month,
      totalAmount: Number(totalAmount),
      totalCount: expenses.length,
      byCategory,
    };
  }
}
