import {
  ForbiddenException,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { ILike, Repository, type FindOptionsOrder } from 'typeorm';
import { unlink } from 'fs/promises';
import { join } from 'path';
import { UserService, type PublicUser } from '../user/user.service';
import {
  paginated,
  resolvePage,
  type PaginatedResult,
} from '../../common/pagination/pagination';
import { Project } from './entities/project.entity';
import { ProjectMember } from './entities/project-member.entity';
import { ProjectAttachment } from './entities/project-attachment.entity';
import { Task } from '../task/entities/task.entity';
import type { AttachmentKind } from '../task/entities/attachment.entity';
import {
  UPLOAD_DIR,
  deriveKind,
  decodeOriginalName,
  type UploadedDiskFile,
} from '../task/upload.config';
import {
  extractAttachmentText,
  isExtractableType,
  type AttachmentAiStatus,
} from './extract-text';
import { CreateProjectDto } from './dto/create-project.dto';
import { UpdateProjectDto } from './dto/update-project.dto';

// 프로젝트 첨부 응답
export interface ProjectAttachmentResponse {
  id: string;
  name: string;
  size: number;
  kind: AttachmentKind;
  url: string;
  uploader: PublicUser | null;
  createdAt: string;
  // AI 추천 반영 상태 — 추출 본문은 노출하지 않고 상태 플래그만 제공
  aiStatus: AttachmentAiStatus;
}

// 프로젝트 응답 형태 — 파생 라벨/진행률은 프론트에서 계산
export interface ProjectResponse {
  id: string;
  name: string;
  description: string | null;
  members: PublicUser[];
  memberCount: number;
  doneCount: number; // 완료 업무 수
  totalCount: number; // 전체 업무 수
  updatedAt: string;
  // 단건(findOne)에서만 — 현재 사용자 권한 + 문서 첨부
  canManage?: boolean; // admin
  isOwner?: boolean; // 소유자
  attachments?: ProjectAttachmentResponse[];
}

// 프로젝트 비즈니스 로직 + 권한 헬퍼(다른 모듈이 주입해 사용)
@Injectable()
export class ProjectService {
  constructor(
    @InjectRepository(Project)
    private readonly projectRepo: Repository<Project>,
    @InjectRepository(ProjectMember)
    private readonly memberRepo: Repository<ProjectMember>,
    @InjectRepository(Task)
    private readonly taskRepo: Repository<Task>,
    @InjectRepository(ProjectAttachment)
    private readonly attachmentRepo: Repository<ProjectAttachment>,
    private readonly userService: UserService,
  ) {}

  // 프로젝트 목록 — 단일 조직 모델상 인증 사용자면 전체 열람. 검색/정렬 + 페이지네이션.
  async findAll(
    page?: number,
    size?: number,
    opts: { sort?: string; q?: string } = {},
  ): Promise<PaginatedResult<ProjectResponse>> {
    const pg = resolvePage(page, size);
    const q = opts.q?.trim();
    // 정렬: 이름순 / 최근 생성순 / 최근 업데이트순(기본)
    const order: FindOptionsOrder<Project> =
      opts.sort === 'name'
        ? { name: 'ASC' }
        : opts.sort === 'created'
          ? { createdAt: 'DESC' }
          : { updatedAt: 'DESC' };
    const [projects, total] = await this.projectRepo.findAndCount({
      relations: ['members', 'members.user'],
      where: q ? { name: ILike(`%${q}%`) } : {},
      order,
      skip: pg.skip,
      take: pg.take,
    });
    const ids = projects.map((p) => p.id);
    const taskMap = await this.taskCountsByProject(ids);
    return paginated(
      projects.map((p) => this.toResponse(p, taskMap)),
      total,
      pg,
    );
  }

  // 프로젝트 단건 — 인증 사용자면 열람 가능. 현재 사용자 권한(canManage/isOwner) 포함.
  async findOne(projectId: string, userId: string): Promise<ProjectResponse> {
    const project = await this.projectRepo.findOne({
      where: { id: projectId },
      relations: ['members', 'members.user'],
    });
    if (!project) {
      throw new NotFoundException();
    }
    const membership = await this.memberRepo.findOne({
      where: { project: { id: project.id }, user: { id: userId } },
    });
    const taskMap = await this.taskCountsByProject([project.id]);
    const base = this.toResponse(project, taskMap);
    return {
      ...base,
      canManage: membership?.roleType === 'admin',
      isOwner: membership?.isOwner ?? false,
      attachments: await this.listFiles(project.id),
    };
  }

  // 프로젝트 생성 — 생성자를 owner-admin 멤버로 등록
  async create(
    dto: CreateProjectDto,
    userId: string,
  ): Promise<ProjectResponse> {
    const project = await this.projectRepo.save(
      this.projectRepo.create({
        name: dto.name,
        description: dto.description ?? null,
      }),
    );
    const creator = await this.userService.findById(userId);
    if (creator) {
      await this.memberRepo.save(
        this.memberRepo.create({
          project,
          user: creator,
          roleType: 'admin',
          isOwner: true,
        }),
      );
    }
    return this.findOne(project.id, userId);
  }

  // 프로젝트 수정 — admin 권한.
  async update(
    projectId: string,
    userId: string,
    dto: UpdateProjectDto,
  ): Promise<ProjectResponse> {
    const project = await this.getProjectOrThrow(projectId);
    await this.assertProjectAdmin(project.id, userId);
    if (dto.name !== undefined) project.name = dto.name;
    if (dto.description !== undefined) project.description = dto.description;
    await this.projectRepo.save(project);
    return this.findOne(project.id, userId);
  }

  // 프로젝트 삭제 — 소유자 권한.
  async remove(projectId: string, userId: string): Promise<void> {
    const project = await this.getProjectOrThrow(projectId);
    const membership = await this.memberRepo.findOne({
      where: { project: { id: project.id }, user: { id: userId } },
    });
    if (!membership?.isOwner) {
      throw new ForbiddenException();
    }
    await this.projectRepo.remove(project);
  }

  // --- 다른 모듈에서 쓰는 헬퍼 ---

  // projectId 로 프로젝트 로딩, 없으면 404
  async getProjectOrThrow(projectId: string): Promise<Project> {
    const project = await this.projectRepo.findOne({
      where: { id: projectId },
    });
    if (!project) {
      throw new NotFoundException();
    }
    return project;
  }

  // 프로젝트 멤버 확인 — 아니면 403
  async assertProjectMember(projectId: string, userId: string): Promise<void> {
    const membership = await this.memberRepo.findOne({
      where: { project: { id: projectId }, user: { id: userId } },
    });
    if (!membership) {
      throw new ForbiddenException();
    }
  }

  // 프로젝트 admin 확인 — 아니면 403
  async assertProjectAdmin(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();
    }
  }

  // --- 문서 첨부 ---

  // 첨부 목록(최신순) — 프로젝트 상세에 포함
  async listFiles(projectId: string): Promise<ProjectAttachmentResponse[]> {
    // extractedText 는 select:false 이지만, AI 반영 상태 배지 산출을 위해 명시적으로 선택한다.
    // (본문은 응답에 담지 않고 상태 판별에만 사용)
    const rows = await this.attachmentRepo
      .createQueryBuilder('a')
      .leftJoinAndSelect('a.uploader', 'uploader')
      .addSelect('a.extractedText')
      .where('a.project_id = :projectId', { projectId })
      .orderBy('a.created_at', 'DESC')
      .getMany();
    return rows.map((a) => this.toAttachmentResponse(a, projectId));
  }

  // AI 업무 추천용 — 프로젝트 문서들의 캐싱된 추출 본문을 문서명과 함께 합쳐 반환한다.
  // 총량 상한으로 잘라 토큰 사용을 보호하며, 추출 텍스트가 없으면 빈 문자열.
  // (extractedText 는 select:false 이므로 명시적으로 선택해 조회한다.)
  async getAttachmentsTextForAi(projectId: string): Promise<string> {
    const TOTAL_LIMIT = 16000;
    const rows = await this.attachmentRepo
      .createQueryBuilder('a')
      .select(['a.name', 'a.extractedText'])
      .where('a.project_id = :projectId', { projectId })
      .orderBy('a.created_at', 'ASC')
      .getMany();

    const parts: string[] = [];
    let used = 0;
    for (const r of rows) {
      const text = r.extractedText?.trim();
      if (!text) continue;
      const remaining = TOTAL_LIMIT - used;
      if (remaining <= 0) break;
      const slice = text.length > remaining ? text.slice(0, remaining) : text;
      parts.push(`### ${r.name}\n${slice}`);
      used += slice.length;
    }
    return parts.join('\n\n');
  }

  // 첨부 업로드 — 관리자만. 디스크 저장은 multer 가 처리, 메타데이터만 영속
  async addFile(
    projectId: string,
    userId: string,
    file: UploadedDiskFile,
  ): Promise<ProjectAttachmentResponse> {
    const project = await this.getProjectOrThrow(projectId);
    await this.assertProjectAdmin(project.id, userId);
    const uploader = await this.userService.findById(userId);

    const name = decodeOriginalName(file.originalname);
    // 업로드 시점에 텍스트를 1회 추출해 캐싱 — AI 추천에서 재파싱 없이 꺼내 쓴다.
    const extractedText = await extractAttachmentText(
      file.path,
      file.mimetype,
      name,
    );
    const saved = await this.attachmentRepo.save(
      this.attachmentRepo.create({
        project: { id: project.id } as Project,
        uploader: uploader ?? null,
        name,
        storedName: file.filename,
        size: file.size,
        mime: file.mimetype,
        kind: deriveKind(file.mimetype, name),
        extractedText,
      }),
    );
    return this.toAttachmentResponse(
      { ...saved, uploader: uploader ?? null },
      project.id,
    );
  }

  // 첨부 삭제 — 관리자만. 디스크 파일도 정리
  async removeFile(
    projectId: string,
    userId: string,
    fileId: string,
  ): Promise<void> {
    const project = await this.getProjectOrThrow(projectId);
    await this.assertProjectAdmin(project.id, userId);
    const attachment = await this.attachmentRepo.findOne({
      where: { id: fileId, project: { id: project.id } },
    });
    if (!attachment) {
      throw new NotFoundException();
    }
    await this.attachmentRepo.remove(attachment);
    await this.deleteFileFromDisk(attachment.storedName);
  }

  // 다운로드용 첨부 메타 + 디스크 절대경로 — 멤버면 조회 가능. 컨트롤러가 스트리밍
  async getFileForDownload(
    projectId: string,
    userId: string,
    fileId: string,
  ): Promise<{ attachment: ProjectAttachment; absolutePath: string }> {
    const project = await this.getProjectOrThrow(projectId);
    await this.assertProjectMember(project.id, userId);
    const attachment = await this.attachmentRepo.findOne({
      where: { id: fileId, project: { id: project.id } },
    });
    if (!attachment) {
      throw new NotFoundException();
    }
    return {
      attachment,
      absolutePath: join(UPLOAD_DIR, attachment.storedName),
    };
  }

  // 디스크 파일 삭제(베스트 에포트 — 없어도 무시)
  private async deleteFileFromDisk(storedName: string): Promise<void> {
    try {
      await unlink(join(UPLOAD_DIR, storedName));
    } catch {
      // 이미 없거나 권한 문제 — 본 흐름을 막지 않는다
    }
  }

  private toAttachmentResponse(
    att: ProjectAttachment,
    projectId: string,
  ): ProjectAttachmentResponse {
    return {
      id: att.id,
      name: att.name,
      size: att.size,
      kind: att.kind,
      url: `/api/projects/${projectId}/files/${att.id}/download`,
      uploader: att.uploader ? UserService.toPublic(att.uploader) : null,
      createdAt: att.createdAt.toISOString(),
      aiStatus: this.deriveAiStatus(att),
    };
  }

  // 첨부의 AI 반영 상태 산출 — 추출 본문 유무 + 형식으로 판별
  private deriveAiStatus(att: ProjectAttachment): AttachmentAiStatus {
    if (att.extractedText && att.extractedText.trim().length > 0) {
      return 'ready';
    }
    // 본문이 비어 있음 — 지원 형식이면 추출 실패, 아니면 미지원 형식
    return isExtractableType(att.mime, att.name) ? 'failed' : 'unsupported';
  }

  // 프로젝트별 업무 완료/전체 수 — 진행률 집계용
  private async taskCountsByProject(
    ids: string[],
  ): Promise<Map<string, { done: number; total: number }>> {
    const map = new Map<string, { done: number; total: number }>();
    if (ids.length === 0) return map;
    const rows = await this.taskRepo
      .createQueryBuilder('task')
      .select('task.project_id', 'projectId')
      .addSelect('COUNT(*)', 'total')
      .addSelect("COUNT(*) FILTER (WHERE task.status = 'done')", 'done')
      .where('task.project_id IN (:...ids)', { ids })
      .groupBy('task.project_id')
      .getRawMany<{ projectId: string; total: string; done: string }>();
    for (const r of rows) {
      map.set(r.projectId, { done: Number(r.done), total: Number(r.total) });
    }
    return map;
  }

  // 엔티티 → 응답 매핑. 카운트 맵에서 프로젝트별 집계를 주입한다.
  private toResponse(
    project: Project,
    taskMap: Map<string, { done: number; total: number }>,
  ): ProjectResponse {
    const members = project.members ?? [];
    const counts = taskMap.get(project.id);
    return {
      id: project.id,
      name: project.name,
      description: project.description,
      members: members
        .filter((m) => m.user)
        .map((m) => UserService.toPublic(m.user)),
      memberCount: members.length,
      doneCount: counts?.done ?? 0,
      totalCount: counts?.total ?? 0,
      updatedAt: project.updatedAt.toISOString(),
    };
  }
}
