import {
  ForbiddenException,
  HttpStatus,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { UserService, type PublicUser } from '../user/user.service';
import { BusinessException } from '../../common/exceptions/business.exception';
import {
  paginated,
  resolvePage,
  type PaginatedResult,
} from '../../common/pagination/pagination';
import { Project } from './entities/project.entity';
import {
  ProjectMember,
  type ProjectRoleType,
} from './entities/project-member.entity';
import { Task } from '../task/entities/task.entity';
import { ActivityService } from '../activity/activity.service';
import { NotificationService } from '../notification/notification.service';
import { InviteProjectMemberDto } from './dto/invite-member.dto';
import { UpdateProjectMemberDto } from './dto/update-member.dto';

// 프로젝트 멤버 응답 — isYou 는 프론트가 현재 사용자와 비교해 산출
export interface ProjectMemberResponse {
  user: PublicUser;
  email: string;
  taskCount: number; // 프로젝트 내 담당 업무 수
  roleType: ProjectRoleType;
  owner: boolean;
}

// 프로젝트 멤버 관리 비즈니스 로직 — 초대/역할변경/제거 시 활동 적재·알림 발송
@Injectable()
export class ProjectMemberService {
  constructor(
    @InjectRepository(Project)
    private readonly projectRepo: Repository<Project>,
    @InjectRepository(ProjectMember)
    private readonly memberRepo: Repository<ProjectMember>,
    @InjectRepository(Task)
    private readonly taskRepo: Repository<Task>,
    private readonly userService: UserService,
    private readonly activity: ActivityService,
    private readonly notification: NotificationService,
  ) {}

  // 멤버 목록 — 인증 사용자면 열람. 쓰기는 admin 가드.
  async list(
    projectId: string,
    page?: number,
    size?: number,
  ): Promise<PaginatedResult<ProjectMemberResponse>> {
    const project = await this.getProjectOrThrow(projectId);
    const pg = resolvePage(page, size);
    const [members, total] = await this.memberRepo.findAndCount({
      where: { project: { id: project.id } },
      relations: ['user'],
      order: { createdAt: 'ASC' },
      skip: pg.skip,
      take: pg.take,
    });
    const counts = await this.assigneeCounts(project.id);
    return paginated(
      members.map((m) => this.toResponse(m, counts.get(m.user.id) ?? 0)),
      total,
      pg,
    );
  }

  // 멤버 초대 — admin 권한. 이미 가입된 사용자만 이메일로 초대
  async invite(
    projectId: string,
    actingUserId: string,
    dto: InviteProjectMemberDto,
  ): Promise<ProjectMemberResponse> {
    const project = await this.getProjectOrThrow(projectId);
    await this.assertAdmin(project.id, actingUserId);

    const user = await this.userService.findByEmail(dto.email);
    if (!user) {
      throw new BusinessException(
        'BIZ_001',
        '해당 이메일의 가입된 사용자를 찾을 수 없습니다. 먼저 가입이 필요합니다.',
        HttpStatus.NOT_FOUND,
      );
    }

    const existing = await this.memberRepo.findOne({
      where: { project: { id: project.id }, user: { id: user.id } },
    });
    if (existing) {
      throw new BusinessException(
        'BIZ_001',
        '이미 프로젝트에 속한 멤버입니다.',
        HttpStatus.CONFLICT,
      );
    }

    const member = this.memberRepo.create({
      project,
      user,
      roleType: dto.roleType,
      isOwner: false,
    });
    const saved = await this.memberRepo.save(member);
    saved.user = user;

    // 활동 적재 + 초대 대상에게 알림
    await this.activity.record({
      projectId: project.id,
      actorId: actingUserId,
      type: 'member.invited',
      payload: { targetName: user.name },
    });
    await this.notification.notify({
      recipientIds: [user.id],
      actorId: actingUserId,
      type: 'member.invited',
      payload: {
        projectId: project.id,
        projectName: project.name,
        actorName: await this.actorName(actingUserId),
      },
    });
    return this.toResponse(saved);
  }

  // 멤버 역할 변경 — admin 권한. 소유자는 변경 불가
  async update(
    projectId: string,
    actingUserId: string,
    targetUserId: string,
    dto: UpdateProjectMemberDto,
  ): Promise<ProjectMemberResponse> {
    const project = await this.getProjectOrThrow(projectId);
    await this.assertAdmin(project.id, actingUserId);
    const member = await this.getMemberOrThrow(project.id, targetUserId);
    if (member.isOwner) {
      throw new BusinessException(
        'BIZ_001',
        '소유자의 권한은 변경할 수 없습니다.',
        HttpStatus.FORBIDDEN,
      );
    }
    if (dto.roleType !== undefined) member.roleType = dto.roleType;
    await this.memberRepo.save(member);

    // 활동 적재 + 역할 변경 대상에게 알림
    await this.activity.record({
      projectId: project.id,
      actorId: actingUserId,
      type: 'member.role_changed',
      payload: { targetName: member.user.name, roleType: member.roleType },
    });
    await this.notification.notify({
      recipientIds: [targetUserId],
      actorId: actingUserId,
      type: 'member.role_changed',
      payload: {
        projectId: project.id,
        projectName: project.name,
        roleType: member.roleType,
        actorName: await this.actorName(actingUserId),
      },
    });
    return this.toResponse(member);
  }

  // 멤버 제거 — admin 권한. 소유자는 제거 불가
  async remove(
    projectId: string,
    actingUserId: string,
    targetUserId: string,
  ): Promise<void> {
    const project = await this.getProjectOrThrow(projectId);
    await this.assertAdmin(project.id, actingUserId);
    const member = await this.getMemberOrThrow(project.id, targetUserId);
    if (member.isOwner) {
      throw new BusinessException(
        'BIZ_001',
        '소유자는 프로젝트에서 제거할 수 없습니다.',
        HttpStatus.FORBIDDEN,
      );
    }
    await this.memberRepo.remove(member);

    // 활동 적재 + 제외 대상에게 알림(remove 직후 메모리의 user 사용)
    await this.activity.record({
      projectId: project.id,
      actorId: actingUserId,
      type: 'member.removed',
      payload: { targetName: member.user.name },
    });
    await this.notification.notify({
      recipientIds: [targetUserId],
      actorId: actingUserId,
      type: 'member.removed',
      payload: {
        projectId: project.id,
        projectName: project.name,
        actorName: await this.actorName(actingUserId),
      },
    });
  }

  // --- 내부 헬퍼 ---

  // 프로젝트 내 사용자별 담당 업무 수 — 멤버 목록 taskCount 집계용
  private async assigneeCounts(
    projectId: string,
  ): Promise<Map<string, number>> {
    const rows = await this.taskRepo
      .createQueryBuilder('task')
      .innerJoin('task_assignees', 'ta', 'ta.task_id = task.id')
      .select('ta.user_id', 'userId')
      .addSelect('COUNT(*)', 'count')
      .where('task.project_id = :projectId', { projectId })
      .groupBy('ta.user_id')
      .getRawMany<{ userId: string; count: string }>();
    const map = new Map<string, number>();
    for (const r of rows) map.set(r.userId, Number(r.count));
    return map;
  }

  private async getProjectOrThrow(projectId: string): Promise<Project> {
    const project = await this.projectRepo.findOne({
      where: { id: projectId },
    });
    if (!project) {
      throw new NotFoundException();
    }
    return project;
  }

  // 행위자 표시 이름 — 알림 payload 의 actorName 용. 없으면 '' (프론트가 '누군가'로 폴백)
  private async actorName(userId: string): Promise<string> {
    const user = await this.userService.findById(userId);
    return user?.name ?? '';
  }

  private async getMemberOrThrow(
    projectId: string,
    userId: string,
  ): Promise<ProjectMember> {
    const member = await this.memberRepo.findOne({
      where: { project: { id: projectId }, user: { id: userId } },
      relations: ['user'],
    });
    if (!member) {
      throw new NotFoundException();
    }
    return member;
  }

  private async assertAdmin(projectId: string, userId: string): Promise<void> {
    const membership = await this.memberRepo.findOne({
      where: { project: { id: projectId }, user: { id: userId } },
    });
    if (!membership || membership.roleType !== 'admin') {
      throw new ForbiddenException();
    }
  }

  private toResponse(
    member: ProjectMember,
    taskCount = 0,
  ): ProjectMemberResponse {
    return {
      user: UserService.toPublic(member.user),
      email: member.user.email,
      taskCount,
      roleType: member.roleType,
      owner: member.isOwner,
    };
  }
}
