import { ConflictException, Injectable, NotFoundException } from '@nestjs/common';
import { and, asc, eq, inArray, sql } from 'drizzle-orm';
import { DatabaseService } from '../../common/database/database.service';
import { watchlistItems, watchlists } from '../../database/schema';
import type { CreateWatchlistDto, UpdateWatchlistDto, WatchlistItemDto } from './dto/watchlist.dto';

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

  async list(ownerId: string) {
    const rows = await this.database.db
      .select()
      .from(watchlists)
      .where(eq(watchlists.ownerId, ownerId))
      .orderBy(asc(watchlists.createdAt));
    return this.hydrate(rows);
  }

  async create(ownerId: string, input: CreateWatchlistDto) {
    return this.database.db.transaction(async (tx) => {
      const [watchlist] = await tx
        .insert(watchlists)
        .values({ ownerId, name: input.name.trim() })
        .returning();
      await this.insertItems(tx, watchlist.id, input.items);
      const [hydrated] = await this.hydrate([watchlist], tx);
      return hydrated;
    });
  }

  async update(ownerId: string, id: string, input: UpdateWatchlistDto) {
    await this.requireWatchlist(ownerId, id);
    return this.database.db.transaction(async (tx) => {
      const [watchlist] = await tx
        .update(watchlists)
        .set({
          ...(input.name !== undefined ? { name: input.name.trim() } : {}),
          version: sql`${watchlists.version} + 1`,
          updatedAt: new Date(),
        })
        .where(
          and(
            eq(watchlists.id, id),
            eq(watchlists.ownerId, ownerId),
            eq(watchlists.version, input.version),
          ),
        )
        .returning();

      if (!watchlist) {
        throw new ConflictException(
          `Watchlist was changed by another request (expected version ${input.version}).`,
        );
      }

      if (input.items !== undefined) {
        await tx.delete(watchlistItems).where(eq(watchlistItems.watchlistId, id));
        await this.insertItems(tx, id, input.items);
      }

      const [hydrated] = await this.hydrate([watchlist], tx);
      return hydrated;
    });
  }

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

  private async hydrate(
    rows: (typeof watchlists.$inferSelect)[],
    database: Pick<DatabaseService['db'], 'select'> = this.database.db,
  ) {
    if (!rows.length) return [];
    const items = await database
      .select()
      .from(watchlistItems)
      .where(
        inArray(
          watchlistItems.watchlistId,
          rows.map((row) => row.id),
        ),
      )
      .orderBy(asc(watchlistItems.position));

    return rows.map((watchlist) => ({
      ...watchlist,
      items: items
        .filter((item) => item.watchlistId === watchlist.id)
        .map((item) => ({
          id: item.id,
          coinId: item.coinId,
          symbol: item.symbol,
          position: item.position,
        })),
    }));
  }

  private async insertItems(
    tx: Parameters<Parameters<DatabaseService['db']['transaction']>[0]>[0],
    watchlistId: string,
    items: WatchlistItemDto[],
  ): Promise<void> {
    if (!items.length) return;
    await tx.insert(watchlistItems).values(
      items.map((item, position) => ({
        watchlistId,
        coinId: item.coinId,
        symbol: item.symbol,
        position,
      })),
    );
  }

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