import {
  BadRequestException,
  ForbiddenException,
  Injectable,
  NotFoundException,
  OnModuleInit,
} from '@nestjs/common';
import { GameStatus, ProjectStatus, UserRole } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { AssignProjectParticipantDto } from './dto/assign-project-participant.dto';
import { CloneProjectDto } from './dto/clone-project.dto';
import { CreateProjectDto } from './dto/create-project.dto';
import { UpdateProjectDto } from './dto/update-project.dto';
import {
  DEFAULT_PROJECT_ID,
  DEFAULT_WORKSPACE_ID,
  DEFAULT_WORKSPACE_NAME,
  DEFAULT_WORKSPACE_SLUG,
  MAX_PROJECT_BABIES,
} from './project-scope.constants';

@Injectable()
export class ProjectsService implements OnModuleInit {
  constructor(private readonly prismaService: PrismaService) {}

  async onModuleInit() {
    await this.ensureDefaultWorkspace();
  }

  private clampBabyCount(value?: number) {
    if (!value) {
      return 1;
    }

    return Math.min(MAX_PROJECT_BABIES, Math.max(1, Math.trunc(value)));
  }

  private mapProjectStatusToGameStatus(status: ProjectStatus): GameStatus {
    if (status === ProjectStatus.CLOSED || status === ProjectStatus.FINALIZED) {
      return GameStatus.CLOSED;
    }

    return GameStatus.OPEN;
  }

  private normalizeSlug(value: string) {
    const normalized = value
      .trim()
      .toLowerCase()
      .normalize('NFD')
      .replace(/[\u0300-\u036f]/g, '')
      .replace(/[^a-z0-9]+/g, '-')
      .replace(/^-+|-+$/g, '');

    return normalized || `workspace-${Date.now()}`;
  }

  private async ensureWorkspaceForAdmin(userId: string) {
    const user = await this.prismaService.user.findUnique({
      where: { id: userId },
      select: { id: true, role: true, workspaceId: true, username: true },
    });

    if (!user) {
      throw new NotFoundException('Utilisateur introuvable');
    }

    if (user.role !== UserRole.ADMIN) {
      throw new ForbiddenException('Seuls les comptes admin peuvent creer des projets');
    }

    if (user.workspaceId) {
      return user.workspaceId;
    }

    const baseSlug = this.normalizeSlug(user.username);
    const workspace = await this.prismaService.workspace.create({
      data: {
        name: `Espace ${user.username}`,
        slug: `${baseSlug}-${Date.now()}`,
        ownerUserId: user.id,
      },
    });

    await this.prismaService.user.update({
      where: { id: user.id },
      data: { workspaceId: workspace.id },
    });

    return workspace.id;
  }

  private async ensureProjectBabies(projectId: string, babyCount: number, babyLabels?: string[]) {
    const safeCount = this.clampBabyCount(babyCount);

    for (let index = 1; index <= safeCount; index += 1) {
      await this.prismaService.projectBaby.upsert({
        where: {
          projectId_babyIndex: {
            projectId,
            babyIndex: index,
          },
        },
        update: {
          label: babyLabels?.[index - 1] ?? `Bebe ${index}`,
        },
        create: {
          projectId,
          babyIndex: index,
          label: babyLabels?.[index - 1] ?? `Bebe ${index}`,
        },
      });
    }

    await this.prismaService.projectBaby.deleteMany({
      where: {
        projectId,
        babyIndex: { gt: safeCount },
      },
    });
  }

  private async assertProjectAccess(
    userId: string,
    projectId: string,
    options?: { adminOnly?: boolean },
  ) {
    const [user, project, membership] = await Promise.all([
      this.prismaService.user.findUnique({
        where: { id: userId },
        select: { id: true, role: true, workspaceId: true },
      }),
      this.prismaService.project.findUnique({
        where: { id: projectId },
      }),
      this.prismaService.projectMembership.findUnique({
        where: { userId },
        select: { id: true, projectId: true },
      }),
    ]);

    if (!user) {
      throw new NotFoundException('Utilisateur introuvable');
    }

    if (!project) {
      throw new NotFoundException('Projet introuvable');
    }

    if (!user.workspaceId || user.workspaceId !== project.workspaceId) {
      throw new ForbiddenException('Acces refuse a ce projet');
    }

    if (user.role === UserRole.ADMIN) {
      return { user, project, isAdmin: true };
    }

    if (options?.adminOnly) {
      throw new ForbiddenException('Action reservee aux admins');
    }

    if (!membership || membership.projectId !== project.id) {
      throw new ForbiddenException('Vous n etes pas rattache a ce projet');
    }

    return { user, project, isAdmin: false };
  }

  private async copyCardsFromProject(params: {
    sourceProjectId: string;
    targetProjectId: string;
    targetGameId: string;
    actorUserId: string;
    filterLegacyCodes?: string[];
  }) {
    const sourceProjectFilter =
      params.sourceProjectId === DEFAULT_PROJECT_ID
        ? { OR: [{ projectId: params.sourceProjectId }, { projectId: null }] }
        : { projectId: params.sourceProjectId };

    const sourceCards = await this.prismaService.predictionCard.findMany({
      where: {
        ...sourceProjectFilter,
        ...(params.filterLegacyCodes?.length
          ? {
              code: {
                in: params.filterLegacyCodes,
              },
            }
          : {}),
      },
      include: {
        fields: { orderBy: { sortOrder: 'asc' } },
        options: { orderBy: { sortOrder: 'asc' } },
      },
      orderBy: { sortOrder: 'asc' },
    });

    for (const sourceCard of sourceCards) {
      const newCard = await this.prismaService.predictionCard.create({
        data: {
          gameId: params.targetGameId,
          projectId: params.targetProjectId,
          code: sourceCard.code,
          title: sourceCard.title,
          description: sourceCard.description,
          type: sourceCard.type,
          valueType: sourceCard.valueType,
          styleId: sourceCard.styleId,
          unit: sourceCard.unit,
          isActive: sourceCard.isActive,
          isDeletable: sourceCard.isDeletable,
          sortOrder: sourceCard.sortOrder,
          basePoints: sourceCard.basePoints,
          createdById: params.actorUserId,
        },
      });

      for (const field of sourceCard.fields) {
        await this.prismaService.predictionCardField.create({
          data: {
            cardId: newCard.id,
            label: field.label,
            sortOrder: field.sortOrder,
            points: field.points,
            isPrimary: field.isPrimary,
            isRequired: field.isRequired,
            minNumber: field.minNumber,
            maxNumber: field.maxNumber,
            stepNumber: field.stepNumber,
          },
        });
      }

      for (const option of sourceCard.options) {
        await this.prismaService.predictionCardOption.create({
          data: {
            cardId: newCard.id,
            label: option.label,
            value: option.value,
            sortOrder: option.sortOrder,
          },
        });
      }
    }
  }

  private async assignParticipantToProjectInternal(
    actorUserId: string,
    projectId: string,
    dto: AssignProjectParticipantDto,
  ) {
    const project = await this.prismaService.project.findUnique({
      where: { id: projectId },
      select: { id: true, workspaceId: true },
    });

    if (!project) {
      throw new NotFoundException('Projet introuvable');
    }

    const participant = await this.prismaService.user.findUnique({
      where: { id: dto.userId },
      select: {
        id: true,
        role: true,
        workspaceId: true,
        username: true,
        displayName: true,
      },
    });

    if (!participant) {
      throw new NotFoundException('Compte participant introuvable');
    }

    if (participant.role === UserRole.ADMIN) {
      throw new BadRequestException('Un admin ne peut pas etre rattache comme participant');
    }

    if (participant.workspaceId && participant.workspaceId !== project.workspaceId) {
      throw new ForbiddenException('Ce participant depend d un autre espace');
    }

    if (!participant.workspaceId) {
      await this.prismaService.user.update({
        where: { id: participant.id },
        data: { workspaceId: project.workspaceId },
      });
    }

    const existingMembership = await this.prismaService.projectMembership.findUnique({
      where: { userId: participant.id },
      include: {
        user: {
          select: {
            id: true,
            username: true,
            displayName: true,
            role: true,
          },
        },
      },
    });

    if (existingMembership) {
      if (existingMembership.projectId !== projectId) {
        return this.prismaService.projectMembership.update({
          where: { userId: participant.id },
          data: {
            projectId,
            assignedById: actorUserId,
          },
          include: {
            user: {
              select: {
                id: true,
                username: true,
                displayName: true,
                role: true,
              },
            },
          },
        });
      }

      return existingMembership;
    }

    return this.prismaService.projectMembership.create({
      data: {
        projectId,
        userId: participant.id,
        assignedById: actorUserId,
      },
      include: {
        user: {
          select: {
            id: true,
            username: true,
            displayName: true,
            role: true,
          },
        },
      },
    });
  }

  async ensureDefaultWorkspace() {
    const firstAdmin = await this.prismaService.user.findFirst({
      where: { role: UserRole.ADMIN },
      orderBy: { createdAt: 'asc' },
      select: {
        id: true,
        workspaceId: true,
      },
    });

    if (!firstAdmin) {
      return;
    }

    await this.prismaService.workspace.upsert({
      where: { id: DEFAULT_WORKSPACE_ID },
      update: {
        name: DEFAULT_WORKSPACE_NAME,
        slug: DEFAULT_WORKSPACE_SLUG,
        ownerUserId: firstAdmin.id,
      },
      create: {
        id: DEFAULT_WORKSPACE_ID,
        name: DEFAULT_WORKSPACE_NAME,
        slug: DEFAULT_WORKSPACE_SLUG,
        ownerUserId: firstAdmin.id,
      },
    });

    if (firstAdmin.workspaceId !== DEFAULT_WORKSPACE_ID) {
      await this.prismaService.user.update({
        where: { id: firstAdmin.id },
        data: { workspaceId: DEFAULT_WORKSPACE_ID },
      });
    }
  }

  async listForUser(userId: string) {
    const user = await this.prismaService.user.findUnique({
      where: { id: userId },
      select: { id: true, role: true, workspaceId: true },
    });

    if (!user) {
      throw new NotFoundException('Utilisateur introuvable');
    }

    if (!user.workspaceId) {
      return [];
    }

    if (user.role === UserRole.ADMIN) {
      return this.prismaService.project.findMany({
        where: { workspaceId: user.workspaceId },
        include: {
          babies: { orderBy: { babyIndex: 'asc' } },
          _count: {
            select: {
              memberships: true,
              cards: true,
            },
          },
        },
        orderBy: { createdAt: 'desc' },
      });
    }

    const memberships = await this.prismaService.projectMembership.findMany({
      where: { userId },
      include: {
        project: {
          include: {
            babies: { orderBy: { babyIndex: 'asc' } },
            _count: {
              select: {
                memberships: true,
                cards: true,
              },
            },
          },
        },
      },
      orderBy: { createdAt: 'desc' },
    });

    return memberships.map((membership) => membership.project);
  }

  async getByIdForUser(userId: string, projectId: string) {
    await this.assertProjectAccess(userId, projectId);

    const project = await this.prismaService.project.findUnique({
      where: { id: projectId },
      include: {
        babies: { orderBy: { babyIndex: 'asc' } },
        _count: {
          select: {
            memberships: true,
            cards: true,
          },
        },
      },
    });

    if (!project) {
      throw new NotFoundException('Projet introuvable');
    }

    return project;
  }

  async create(adminUserId: string, dto: CreateProjectDto) {
    const workspaceId = await this.ensureWorkspaceForAdmin(adminUserId);
    const babyCount = this.clampBabyCount(dto.babyCount);

    const project = await this.prismaService.project.create({
      data: {
        workspaceId,
        createdById: adminUserId,
        name: dto.name.trim(),
        description: dto.description?.trim() || null,
        status: dto.status ?? ProjectStatus.OPEN,
        babyCount,
      },
    });

    await this.ensureProjectBabies(project.id, babyCount, dto.babyLabels);

    const game = await this.prismaService.predictionGame.create({
      data: {
        id: project.id,
        projectId: project.id,
        title: `Pronostics ${project.name}`,
        status: this.mapProjectStatusToGameStatus(project.status),
      },
    });

    await this.copyCardsFromProject({
      sourceProjectId: DEFAULT_PROJECT_ID,
      targetProjectId: project.id,
      targetGameId: game.id,
      actorUserId: adminUserId,
      filterLegacyCodes: dto.enabledLegacyCardCodes,
    });

    if (dto.participantUserIds?.length) {
      for (const participantUserId of dto.participantUserIds) {
        await this.assignParticipantToProjectInternal(adminUserId, project.id, {
          userId: participantUserId,
        });
      }
    }

    return this.getByIdForUser(adminUserId, project.id);
  }

  async update(adminUserId: string, projectId: string, dto: UpdateProjectDto) {
    const { project } = await this.assertProjectAccess(adminUserId, projectId, {
      adminOnly: true,
    });

    const babyCount = dto.babyCount ? this.clampBabyCount(dto.babyCount) : project.babyCount;

    const updatedProject = await this.prismaService.project.update({
      where: { id: projectId },
      data: {
        name: dto.name?.trim(),
        description: dto.description?.trim() ?? (dto.description === '' ? null : undefined),
        status: dto.status,
        babyCount,
      },
    });

    if (dto.babyCount || dto.babyLabels) {
      await this.ensureProjectBabies(projectId, babyCount, dto.babyLabels);
    }

    if (dto.status) {
      await this.prismaService.predictionGame.updateMany({
        where: { projectId },
        data: {
          status: this.mapProjectStatusToGameStatus(dto.status),
          closedAt: dto.status === ProjectStatus.CLOSED ? new Date() : null,
          reopenedAt:
            dto.status === ProjectStatus.OPEN || dto.status === ProjectStatus.DRAFT
              ? new Date()
              : null,
        },
      });
    }

    return this.getByIdForUser(adminUserId, updatedProject.id);
  }

  async clone(adminUserId: string, sourceProjectId: string, dto: CloneProjectDto) {
    const { project: sourceProject } = await this.assertProjectAccess(
      adminUserId,
      sourceProjectId,
      { adminOnly: true },
    );

    const sourceBabies = await this.prismaService.projectBaby.findMany({
      where: { projectId: sourceProjectId },
      orderBy: { babyIndex: 'asc' },
    });

    const cloneName = dto.name?.trim() || `${sourceProject.name} (copie)`;

    const clonedProject = await this.prismaService.project.create({
      data: {
        workspaceId: sourceProject.workspaceId,
        createdById: adminUserId,
        clonedFromProjectId: sourceProject.id,
        name: cloneName,
        description: dto.description?.trim() || sourceProject.description,
        status: sourceProject.status,
        babyCount: sourceProject.babyCount,
      },
    });

    await this.ensureProjectBabies(
      clonedProject.id,
      sourceProject.babyCount,
      sourceBabies.map((baby) => baby.label ?? `Bebe ${baby.babyIndex}`),
    );

    const clonedGame = await this.prismaService.predictionGame.create({
      data: {
        id: clonedProject.id,
        projectId: clonedProject.id,
        title: `Pronostics ${cloneName}`,
        status: this.mapProjectStatusToGameStatus(clonedProject.status),
      },
    });

    await this.copyCardsFromProject({
      sourceProjectId,
      targetProjectId: clonedProject.id,
      targetGameId: clonedGame.id,
      actorUserId: adminUserId,
    });

    if (dto.includeParticipants) {
      const sourceMemberships = await this.prismaService.projectMembership.findMany({
        where: { projectId: sourceProjectId },
        select: { userId: true },
      });

      for (const membership of sourceMemberships) {
        await this.assignParticipantToProjectInternal(adminUserId, clonedProject.id, {
          userId: membership.userId,
        });
      }
    }

    return this.getByIdForUser(adminUserId, clonedProject.id);
  }

  async listParticipants(userId: string, projectId: string) {
    await this.assertProjectAccess(userId, projectId);

    return this.prismaService.projectMembership.findMany({
      where: { projectId },
      include: {
        user: {
          select: {
            id: true,
            username: true,
            displayName: true,
            profileImageUrl: true,
            profileBgColor: true,
            role: true,
            createdAt: true,
          },
        },
      },
      orderBy: { createdAt: 'asc' },
    });
  }

  async addParticipant(
    adminUserId: string,
    projectId: string,
    dto: AssignProjectParticipantDto,
  ) {
    await this.assertProjectAccess(adminUserId, projectId, { adminOnly: true });
    return this.assignParticipantToProjectInternal(adminUserId, projectId, dto);
  }

  async removeParticipant(adminUserId: string, projectId: string, participantUserId: string) {
    await this.assertProjectAccess(adminUserId, projectId, { adminOnly: true });

    const result = await this.prismaService.projectMembership.deleteMany({
      where: {
        projectId,
        userId: participantUserId,
      },
    });

    return { removed: result.count > 0 };
  }

  async updateProjectImage(
    adminUserId: string,
    projectId: string,
    projectImageUrl?: string | null,
    projectBgColor?: string | null,
  ) {
    await this.assertProjectAccess(adminUserId, projectId, { adminOnly: true });

    const data: { projectImageUrl?: string | null; projectBgColor?: string | null } = {};
    if (projectImageUrl !== undefined) data.projectImageUrl = projectImageUrl;
    if (projectBgColor !== undefined) data.projectBgColor = projectBgColor;

    await this.prismaService.project.update({
      where: { id: projectId },
      data,
    });

    return this.getByIdForUser(adminUserId, projectId);
  }
}
