import { ConflictException, Injectable, NotFoundException } from '@nestjs/common';
import { and, asc, eq, sql } from 'drizzle-orm';
import { DatabaseService } from '../../common/database/database.service';
import { portfolios, portfolioTransactions } from '../../database/schema';
import type {
  CreatePortfolioTransactionDto,
  UpdatePortfolioTransactionDto,
} from './dto/portfolio-transaction.dto';
import type { CreatePortfolioDto, UpdatePortfolioDto } from './dto/portfolio.dto';

@Injectable()
export class PortfolioRepository {
  constructor(private readonly database: DatabaseService) {}

  list(ownerId: string) {
    return this.database.db
      .select()
      .from(portfolios)
      .where(eq(portfolios.ownerId, ownerId))
      .orderBy(asc(portfolios.createdAt));
  }

  async create(ownerId: string, input: CreatePortfolioDto) {
    const [portfolio] = await this.database.db
      .insert(portfolios)
      .values({
        ownerId,
        name: input.name.trim(),
        note: input.note?.trim() || null,
      })
      .returning();
    return portfolio;
  }

  async update(ownerId: string, id: string, input: UpdatePortfolioDto) {
    const [portfolio] = await this.database.db
      .update(portfolios)
      .set({
        ...(input.name !== undefined ? { name: input.name.trim() } : {}),
        ...(input.note !== undefined ? { note: input.note.trim() || null } : {}),
        version: sql`${portfolios.version} + 1`,
        updatedAt: new Date(),
      })
      .where(
        and(
          eq(portfolios.id, id),
          eq(portfolios.ownerId, ownerId),
          eq(portfolios.version, input.version),
        ),
      )
      .returning();

    if (!portfolio) {
      const current = await this.requirePortfolio(ownerId, id);
      throw new ConflictException(
        `Portfolio was changed by another request (expected version ${input.version}, current version ${current.version}).`,
      );
    }
    return portfolio;
  }

  async delete(ownerId: string, id: string): Promise<void> {
    const deleted = await this.database.db
      .delete(portfolios)
      .where(and(eq(portfolios.id, id), eq(portfolios.ownerId, ownerId)))
      .returning({ id: portfolios.id });
    if (!deleted.length) throw new NotFoundException('Portfolio not found.');
  }

  async listTransactions(ownerId: string, portfolioId: string) {
    await this.requirePortfolio(ownerId, portfolioId);
    const rows = await this.database.db
      .select()
      .from(portfolioTransactions)
      .where(eq(portfolioTransactions.portfolioId, portfolioId))
      .orderBy(asc(portfolioTransactions.date), asc(portfolioTransactions.createdAt));
    return rows.map((row) => this.mapTransaction(row));
  }

  async createTransaction(
    ownerId: string,
    portfolioId: string,
    input: CreatePortfolioTransactionDto,
  ) {
    await this.requirePortfolio(ownerId, portfolioId);
    const [row] = await this.database.db
      .insert(portfolioTransactions)
      .values(this.transactionValues(portfolioId, input))
      .returning();
    return this.mapTransaction(row);
  }

  async updateTransaction(ownerId: string, id: string, input: UpdatePortfolioTransactionDto) {
    await this.requireOwnedTransaction(ownerId, id);
    const [row] = await this.database.db
      .update(portfolioTransactions)
      .set({
        ...this.transactionPatchValues(input),
        version: sql`${portfolioTransactions.version} + 1`,
        updatedAt: new Date(),
      })
      .where(
        and(eq(portfolioTransactions.id, id), eq(portfolioTransactions.version, input.version)),
      )
      .returning();

    if (!row) {
      throw new ConflictException(
        `Portfolio transaction was changed by another request (expected version ${input.version}).`,
      );
    }
    return this.mapTransaction(row);
  }

  async deleteTransaction(ownerId: string, id: string): Promise<void> {
    await this.requireOwnedTransaction(ownerId, id);
    await this.database.db.delete(portfolioTransactions).where(eq(portfolioTransactions.id, id));
  }

  private transactionValues(portfolioId: string, input: CreatePortfolioTransactionDto) {
    return {
      portfolioId,
      type: input.type,
      date: input.date,
      asset: input.asset ?? null,
      quantity: input.qty ?? null,
      price: input.price ?? null,
      currency: input.currency ?? null,
      feeQuantity: input.feeQty ?? null,
      feeAsset: input.feeAsset ?? null,
      cashAmount: input.cashAmount ?? null,
      cashCurrency: input.cashCurrency ?? null,
      note: input.note?.trim() || null,
    };
  }

  private transactionPatchValues(input: UpdatePortfolioTransactionDto) {
    return {
      ...(input.type !== undefined ? { type: input.type } : {}),
      ...(input.date !== undefined ? { date: input.date } : {}),
      ...(input.asset !== undefined ? { asset: input.asset } : {}),
      ...(input.qty !== undefined ? { quantity: input.qty } : {}),
      ...(input.price !== undefined ? { price: input.price } : {}),
      ...(input.currency !== undefined ? { currency: input.currency } : {}),
      ...(input.feeQty !== undefined ? { feeQuantity: input.feeQty } : {}),
      ...(input.feeAsset !== undefined ? { feeAsset: input.feeAsset } : {}),
      ...(input.cashAmount !== undefined ? { cashAmount: input.cashAmount } : {}),
      ...(input.cashCurrency !== undefined ? { cashCurrency: input.cashCurrency } : {}),
      ...(input.note !== undefined ? { note: input.note.trim() || null } : {}),
    };
  }

  private mapTransaction(row: typeof portfolioTransactions.$inferSelect) {
    return {
      id: row.id,
      portfolioId: row.portfolioId,
      type: row.type,
      date: row.date,
      asset: row.asset,
      qty: row.quantity,
      price: row.price,
      currency: row.currency,
      feeQty: row.feeQuantity,
      feeAsset: row.feeAsset,
      cashAmount: row.cashAmount,
      cashCurrency: row.cashCurrency,
      note: row.note,
      version: row.version,
      createdAt: row.createdAt,
      updatedAt: row.updatedAt,
    };
  }

  private async requirePortfolio(ownerId: string, id: string) {
    const [portfolio] = await this.database.db
      .select()
      .from(portfolios)
      .where(and(eq(portfolios.id, id), eq(portfolios.ownerId, ownerId)))
      .limit(1);
    if (!portfolio) throw new NotFoundException('Portfolio not found.');
    return portfolio;
  }

  private async requireOwnedTransaction(ownerId: string, id: string) {
    const [row] = await this.database.db
      .select({ transaction: portfolioTransactions })
      .from(portfolioTransactions)
      .innerJoin(portfolios, eq(portfolios.id, portfolioTransactions.portfolioId))
      .where(and(eq(portfolioTransactions.id, id), eq(portfolios.ownerId, ownerId)))
      .limit(1);
    if (!row) throw new NotFoundException('Portfolio transaction not found.');
    return row.transaction;
  }
}
