import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'
import { InjectModel } from '@nestjs/mongoose'
import { Model, Types } from 'mongoose'
import { Invoice, InvoiceDocument, TimeEntry, TimeEntryDocument, Expense, ExpenseDocument } from '../common/schemas'
import { CreateInvoiceDto } from './dto/create-invoice.dto'
import { GenerateInvoiceDto } from './dto/generate-invoice.dto'

@Injectable()
export class InvoicesService {
  constructor(
    @InjectModel(Invoice.name)   private invoiceModel:  Model<InvoiceDocument>,
    @InjectModel(TimeEntry.name) private timeModel:     Model<TimeEntryDocument>,
    @InjectModel(Expense.name)   private expenseModel:  Model<ExpenseDocument>,
  ) {}

  async create(dto: CreateInvoiceDto): Promise<InvoiceDocument> {
    const invoiceNumber = await this.generateInvoiceNumber()
    const vatRate = dto.vatRate ?? 0.07
    const invoice = new this.invoiceModel({
      ...dto,
      invoiceNumber,
      vatRate,
      clientFolderId: new Types.ObjectId(dto.clientFolderId),
      matterId: dto.matterId ? new Types.ObjectId(dto.matterId) : undefined,
      status: 'draft',
    })
    return invoice.save()
  }

  // Build an invoice from time entries (and optionally expenses) — marks them billed
  async generateFromTimeEntries(dto: GenerateInvoiceDto): Promise<InvoiceDocument> {
    const entries = await this.timeModel.find({
      _id: { $in: dto.timeEntryIds.map(id => new Types.ObjectId(id)) },
      billed: false,
    }).lean().exec()

    if (!entries.length && !(dto.expenseIds?.length)) {
      throw new BadRequestException('No unbilled entries found for those IDs')
    }

    const lineItems: Array<{ description: string; quantity: number; unitPrice: number; amount: number; timeEntryIds?: string[]; expenseIds?: string[] }> = []

    // Time entry line items
    for (const e of entries) {
      lineItems.push({
        description:  e.description,
        quantity:     parseFloat((e.durationMinutes / 60).toFixed(2)),
        unitPrice:    e.ratePerHour,
        amount:       parseFloat(((e.durationMinutes / 60) * e.ratePerHour).toFixed(2)),
        timeEntryIds: [String(e._id)],
      })
    }

    // Expense line items
    if (dto.expenseIds?.length) {
      const expenses = await this.expenseModel.find({
        _id: { $in: dto.expenseIds.map(id => new Types.ObjectId(id)) },
        status: 'pending',
        billable: true,
      }).lean().exec()

      for (const exp of expenses) {
        lineItems.push({
          description: `[Disbursement] ${exp.description}`,
          quantity:    1,
          unitPrice:   exp.amount,
          amount:      exp.amount,
          expenseIds:  [String(exp._id)],
        })
      }
    }

    const subtotal = lineItems.reduce((s, l) => s + l.amount, 0)
    const vatRate = dto.vatRate ?? 0.07
    const vatAmount = parseFloat((subtotal * vatRate).toFixed(2))
    const total = parseFloat((subtotal + vatAmount).toFixed(2))

    const invoiceNumber = await this.generateInvoiceNumber()
    const today = new Date()
    const dueDate = new Date(today)
    dueDate.setDate(dueDate.getDate() + 30)

    const currency = entries[0]?.currency ?? 'THB'
    const invoice = new this.invoiceModel({
      invoiceNumber,
      clientFolderId: new Types.ObjectId(dto.clientFolderId),
      matterId: new Types.ObjectId(dto.matterId),
      lineItems,
      subtotal,
      vatRate,
      vatAmount,
      total,
      currency,
      status: 'draft',
      issueDate: today,
      dueDate,
      notes: dto.notes,
      createdBy: dto.createdBy,
    })

    const saved = await invoice.save()

    // Mark time entries as billed
    if (dto.timeEntryIds.length) {
      await this.timeModel.updateMany(
        { _id: { $in: dto.timeEntryIds.map(id => new Types.ObjectId(id)) } },
        { billed: true, invoiceRef: invoiceNumber },
      ).exec()
    }

    // Mark expenses as billed
    if (dto.expenseIds?.length) {
      await this.expenseModel.updateMany(
        { _id: { $in: dto.expenseIds.map(id => new Types.ObjectId(id)) } },
        { status: 'billed', invoiceId: String(saved._id) },
      ).exec()
    }

    return saved
  }

  async findAll(filters: {
    clientFolderId?: string
    matterId?: string
    status?: string
    limit?: number
    offset?: number
  }): Promise<{ items: InvoiceDocument[]; total: number }> {
    const query: any = {}
    if (filters.clientFolderId) query.clientFolderId = new Types.ObjectId(filters.clientFolderId)
    if (filters.matterId) query.matterId = new Types.ObjectId(filters.matterId)
    if (filters.status) query.status = filters.status

    const [items, total] = await Promise.all([
      this.invoiceModel.find(query)
        .sort({ issueDate: -1 })
        .skip(filters.offset ?? 0)
        .limit(filters.limit ?? 50)
        .exec(),
      this.invoiceModel.countDocuments(query),
    ])
    return { items, total }
  }

  async findOne(id: string): Promise<InvoiceDocument> {
    const inv = await this.invoiceModel.findById(id).exec()
    if (!inv) throw new NotFoundException(`Invoice ${id} not found`)
    return inv
  }

  async markSent(id: string): Promise<InvoiceDocument> {
    const inv = await this.findOne(id)
    if (inv.status !== 'draft') throw new BadRequestException('Only draft invoices can be marked sent')
    return this.invoiceModel.findByIdAndUpdate(id, { status: 'sent' }, { new: true }).exec() as Promise<InvoiceDocument>
  }

  async markPaid(id: string, paidAmount: number, paidDate?: Date): Promise<InvoiceDocument> {
    const inv = await this.findOne(id)
    if (inv.status === 'void') throw new BadRequestException('Cannot mark a voided invoice as paid')
    return this.invoiceModel.findByIdAndUpdate(
      id,
      { status: 'paid', paidAmount, paidDate: paidDate ?? new Date() },
      { new: true },
    ).exec() as Promise<InvoiceDocument>
  }

  async voidInvoice(id: string): Promise<InvoiceDocument> {
    const inv = await this.findOne(id)
    if (inv.status === 'paid') throw new BadRequestException('Cannot void a paid invoice')

    // Unmark time entries as billed
    const timeEntryIds = inv.lineItems
      .flatMap(l => l.timeEntryIds ?? [])
      .map(eid => new Types.ObjectId(eid))

    if (timeEntryIds.length) {
      await this.timeModel.updateMany(
        { _id: { $in: timeEntryIds } },
        { billed: false, $unset: { invoiceRef: '' } },
      ).exec()
    }

    // Unmark expenses
    const expenseIds = inv.lineItems
      .flatMap(l => (l as any).expenseIds ?? [])
      .map(eid => new Types.ObjectId(eid))
    if (expenseIds.length) {
      await this.expenseModel.updateMany(
        { _id: { $in: expenseIds } },
        { status: 'pending', $unset: { invoiceId: '' } },
      ).exec()
    }

    return this.invoiceModel.findByIdAndUpdate(id, { status: 'void' }, { new: true }).exec() as Promise<InvoiceDocument>
  }

  private async generateInvoiceNumber(): Promise<string> {
    const year = new Date().getFullYear()
    const count = await this.invoiceModel.countDocuments({
      invoiceNumber: new RegExp(`^INV-${year}-`),
    })
    return `INV-${year}-${String(count + 1).padStart(4, '0')}`
  }
}
