import { Injectable, NotFoundException, ForbiddenException } from '@nestjs/common';
import { PrismaService } from '../../../prisma/prisma.service';
import { EntityEventsService } from './entity-events.service';
import { EntityPermissions } from './entity-permissions.base';
import { EntityContext } from './entity-context.interface';

export interface PaginatedResult<T> {
  data: T[];
  nextCursor: string | null;
  hasMore: boolean;
}

export interface ListParams {
  cursor?: string;
  limit?: number;
  orderBy?: Record<string, 'asc' | 'desc'>;
}

@Injectable()
export abstract class EntityManager<T extends { id: string }> {
  abstract readonly entityType: string;
  abstract readonly modelName: string;
  abstract readonly include: Record<string, any>;

  constructor(
    protected readonly prisma: PrismaService,
    protected readonly events: EntityEventsService,
    protected readonly permissions: EntityPermissions,
  ) {}

  private get model(): any {
    return (this.prisma as any)[this.modelName];
  }

  // -------------------------------------------------------------------------
  // CRUD
  // -------------------------------------------------------------------------

  async add(data: Record<string, any>, context?: EntityContext): Promise<T> {
    if (!context?.skipPermissions && context?.userId) {
      // Permission check deferred to subclass onBeforeAdd
    }

    let fields = { ...data };
    fields = await this.onBeforeAdd(fields, context);

    await this.events.emitBeforeAdd<T>(this.entityType, fields as any, context);

    const entity = await this.model.create({
      data: fields,
      include: this.include,
    });

    await this.onAfterAdd(entity, context);
    await this.events.emitAfterAdd<T>(this.entityType, entity, context);

    return entity;
  }

  async update(id: string, data: Record<string, any>, context?: EntityContext): Promise<T> {
    const oldEntity = await this.getById(id);
    if (!oldEntity) {
      throw new NotFoundException(`${this.entityType} with id ${id} not found`);
    }

    if (!context?.skipPermissions && context?.userId) {
      const role = (context as any).role || '';
      const clubId = (context as any).clubId || null;
      if (!this.permissions.canUpdate(context.userId, role, clubId, oldEntity)) {
        throw new ForbiddenException(`No permission to update this ${this.entityType}`);
      }
    }

    let fields = { ...data };
    fields = await this.onBeforeUpdate(id, fields, oldEntity, context);

    await this.events.emitBeforeUpdate<T>(this.entityType, id, fields as any, context);

    const entity = await this.model.update({
      where: { id },
      data: fields,
      include: this.include,
    });

    await this.onAfterUpdate(entity, oldEntity, context);
    await this.events.emitAfterUpdate<T>(this.entityType, entity, oldEntity, context);

    return entity;
  }

  async delete(id: string, context?: EntityContext): Promise<void> {
    const entity = await this.getById(id);
    if (!entity) {
      throw new NotFoundException(`${this.entityType} with id ${id} not found`);
    }

    await this.onBeforeDelete(id, context);
    await this.events.emitBeforeDelete(this.entityType, id, context);

    // Soft delete if model has deletedAt, otherwise hard delete
    if ('deletedAt' in (entity as any)) {
      await this.model.update({
        where: { id },
        data: { deletedAt: new Date() },
      });
    } else {
      await this.model.delete({ where: { id } });
    }

    await this.onAfterDelete(id, context);
    await this.events.emitAfterDelete(this.entityType, id, context);
  }

  async getById(id: string): Promise<T | null> {
    return this.model.findUnique({
      where: { id },
      include: this.include,
    });
  }

  async getList(where: Record<string, any>, params: ListParams = {}): Promise<PaginatedResult<T>> {
    const { cursor, limit = 20, orderBy } = params;
    const take = Math.min(limit, 100);

    const items = await this.model.findMany({
      where,
      take: take + 1,
      ...(cursor ? { cursor: { id: cursor }, skip: 1 } : {}),
      orderBy: orderBy || { createdAt: 'desc' },
      include: this.include,
    });

    const hasMore = items.length > take;
    const data = hasMore ? items.slice(0, -1) : items;

    return {
      data,
      nextCursor: hasMore ? data[data.length - 1].id : null,
      hasMore,
    };
  }

  async getCount(where: Record<string, any>): Promise<number> {
    return this.model.count({ where });
  }

  // -------------------------------------------------------------------------
  // Hooks — override in subclass
  // -------------------------------------------------------------------------

  protected async onBeforeAdd(
    fields: Record<string, any>,
    _context?: EntityContext,
  ): Promise<Record<string, any>> {
    return fields;
  }

  protected async onAfterAdd(_entity: T, _context?: EntityContext): Promise<void> {
    // override in subclass
  }

  protected async onBeforeUpdate(
    _id: string,
    fields: Record<string, any>,
    _oldEntity: T,
    _context?: EntityContext,
  ): Promise<Record<string, any>> {
    return fields;
  }

  protected async onAfterUpdate(
    _entity: T,
    _oldEntity: T,
    _context?: EntityContext,
  ): Promise<void> {
    // override in subclass
  }

  protected async onBeforeDelete(_id: string, _context?: EntityContext): Promise<void> {
    // override in subclass
  }

  protected async onAfterDelete(_id: string, _context?: EntityContext): Promise<void> {
    // override in subclass
  }
}
