import { randomBytes } from 'crypto';
import { HttpStatus, Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { InjectRepository } from '@nestjs/typeorm';
import { In, Repository } from 'typeorm';
import { AccessToken } from 'livekit-server-sdk';
import { BusinessException } from '../../common/exceptions/business.exception';
import { UserService, type PublicUser } from '../user/user.service';
import { User } from '../user/entities/user.entity';
import { MeetingRoom } from './entities/meeting-room.entity';
import { CreateMeetingRoomDto } from './dto/create-meeting-room.dto';
import { UpdateMeetingRoomDto } from './dto/update-meeting-room.dto';

// 회의 내 역할 — 진행자(방 생성자) / 일반 참여자
export type MeetingRole = 'host' | 'participant';

// 방 목록/생성 응답 — 진행자·예약·참여제한·접근 가능 여부 포함
export interface MeetingRoomResponse {
  id: string;
  code: string; // 입장 코드(불변) — 입장 URL·초대 링크의 기준
  name: string;
  host: PublicUser | null;
  scheduledAt: string | null;
  restricted: boolean; // 참여 제한(allow-list) 적용 여부
  isHost: boolean; // 현재 사용자가 진행자인지
  canJoin: boolean; // 현재 사용자가 입장 가능한지(공개 OR 진행자 OR 허용목록 포함)
  // 참여 허용 멤버 — 진행자에게만 노출(편집 폼 사전 채움용). 공개 방/비진행자는 null.
  allowedUsers: PublicUser[] | null;
  createdAt: string;
}

// 화상회의 토큰 발급 결과 — 브라우저는 자체 VITE_LIVEKIT_URL 로 접속하므로 URL 은 반환하지 않는다.
export interface MeetingTokenResult {
  token: string; // LiveKit 입장 JWT
  roomName: string;
  identity: string; // 참여자 식별자(사용자 ID)
  role: MeetingRole; // 이번 입장자의 역할
}

// 화상회의 서비스(단계 2: 권한 분기) — LiveKit 입장 토큰(JWT) 발급을 담당한다.
// 미디어 전송은 LiveKit(SFU)에 위임하고, 여기서는 인증된 사용자에게 역할별 권한이 담긴 토큰을 만든다.
// 역할: 방을 처음 만든 사용자가 진행자(host), 이후 입장자는 일반 참여자(participant).
@Injectable()
export class MeetingService {
  private readonly logger = new Logger(MeetingService.name);
  private readonly apiKey: string;
  private readonly apiSecret: string;

  constructor(
    config: ConfigService,
    @InjectRepository(MeetingRoom)
    private readonly roomRepo: Repository<MeetingRoom>,
    @InjectRepository(User)
    private readonly userRepo: Repository<User>,
  ) {
    this.apiKey = (config.get<string>('LIVEKIT_API_KEY') ?? '').trim();
    this.apiSecret = (config.get<string>('LIVEKIT_API_SECRET') ?? '').trim();
    if (!this.enabled) {
      this.logger.warn(
        'LIVEKIT_API_KEY/LIVEKIT_API_SECRET 미설정 — 화상회의 토큰 발급 비활성화',
      );
    }
  }

  get enabled(): boolean {
    return this.apiKey.length > 0 && this.apiSecret.length > 0;
  }

  // LiveKit 입장 토큰 발급 — 역할에 따라 권한(grant)을 차등 부여한다.
  //  · 진행자(host): roomAdmin + 발행/구독 (단계 3의 화면 승격 등 진행자 기능 토대)
  //  · 일반(participant): 발행/구독
  // 역할은 토큰 metadata 에도 실어 다른 참여자가 진행자를 식별할 수 있게 한다.
  async issueToken(
    user: PublicUser,
    code: string,
  ): Promise<MeetingTokenResult> {
    if (!this.enabled) {
      throw new BusinessException(
        'SYS_001',
        '화상회의 서비스가 아직 설정되지 않았습니다. 잠시 후 다시 시도해 주세요.',
        HttpStatus.SERVICE_UNAVAILABLE,
      );
    }

    const { room, role } = await this.resolveRoomAccess(user.id, code);
    const isHost = role === 'host';

    const at = new AccessToken(this.apiKey, this.apiSecret, {
      identity: user.id,
      name: user.name,
      // 장시간 회의 중 네트워크 재연결 시 토큰 만료로 재입장이 실패하지 않도록 넉넉히(12h).
      ttl: '12h',
      // role 은 진행자 식별용, avatarUrl 은 참여자/채팅 아바타 표시용
      metadata: JSON.stringify({ role, avatarUrl: user.avatarUrl }),
    });
    at.addGrant({
      roomJoin: true,
      // LiveKit 방 식별자 = 코드(이름이 아니라 불변 코드라 이름을 바꿔도 같은 방으로 연결).
      room: room.code,
      canPublish: true,
      canSubscribe: true,
      canPublishData: true,
      roomAdmin: isHost,
    });

    const token = await at.toJwt();
    return { token, roomName: room.name, identity: user.id, role };
  }

  // 코드로 방을 찾아 입장 권한을 판정한다(자동 생성 없음 — 명시적으로 생성된 방만 입장 가능).
  //  · 방이 없으면 404. · 참여 제한(allow-list) 검사 후 생성자 일치로 역할을 판정.
  private async resolveRoomAccess(
    userId: string,
    code: string,
  ): Promise<{ room: MeetingRoom; role: MeetingRole }> {
    const room = await this.roomRepo.findOne({
      where: { code },
      relations: ['host'],
    });
    if (!room) {
      throw new BusinessException(
        'RES_001',
        '회의 방을 찾을 수 없습니다. 링크를 확인해 주세요.',
        HttpStatus.NOT_FOUND,
      );
    }
    const isHost = room.host?.id === userId;
    if (
      !isHost &&
      this.isRestricted(room) &&
      !room.allowedUserIds!.includes(userId)
    ) {
      throw new BusinessException(
        'AUTH_003',
        '이 회의에 참여할 권한이 없습니다.',
        HttpStatus.FORBIDDEN,
      );
    }
    return { room, role: isHost ? 'host' : 'participant' };
  }

  // URL-safe 입장 코드(base62, 10자) 생성.
  private generateCode(len = 10): string {
    const ALPHA =
      '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
    const bytes = randomBytes(len);
    let out = '';
    for (let i = 0; i < len; i++) out += ALPHA[bytes[i] % 62];
    return out;
  }

  // 충돌하지 않는 고유 입장 코드 — 드문 충돌 시 재시도, 그래도 충돌하면 길이를 늘린다.
  private async generateUniqueCode(): Promise<string> {
    for (let i = 0; i < 5; i++) {
      const code = this.generateCode();
      const taken = await this.roomRepo.count({ where: { code } });
      if (!taken) return code;
    }
    return this.generateCode(14);
  }

  // 명시적 방 생성 — 생성자가 진행자. 고유 코드 발급 + 예약 시각/참여 제한 설정.
  // 이름은 표시용 라벨이라 중복을 허용한다(식별은 코드로 한다).
  async createRoom(
    userId: string,
    dto: CreateMeetingRoomDto,
  ): Promise<MeetingRoomResponse> {
    const code = await this.generateUniqueCode();
    const saved = await this.roomRepo.save(
      this.roomRepo.create({
        code,
        name: dto.name,
        host: { id: userId } as User,
        scheduledAt: dto.scheduledAt ? new Date(dto.scheduledAt) : null,
        allowedUserIds: dto.allowedUserIds?.length ? dto.allowedUserIds : null,
      }),
    );
    const room = await this.roomRepo.findOne({
      where: { id: saved.id },
      relations: ['host'],
    });
    return this.toRoomResponse(room!, userId);
  }

  // 방 목록 — 전체 방(최근 생성순). 각 방의 접근 가능 여부/진행자 여부를 포함.
  async listRooms(userId: string): Promise<MeetingRoomResponse[]> {
    const rows = await this.roomRepo.find({
      relations: ['host'],
      order: { createdAt: 'DESC' },
    });
    return Promise.all(rows.map((r) => this.toRoomResponse(r, userId)));
  }

  // 방 수정 — 진행자(생성자)만 가능. 이름/예약 시각/참여 제한을 변경한다.
  async updateRoom(
    userId: string,
    roomId: string,
    dto: UpdateMeetingRoomDto,
  ): Promise<MeetingRoomResponse> {
    const room = await this.roomRepo.findOne({
      where: { id: roomId },
      relations: ['host'],
    });
    if (!room) {
      throw new BusinessException(
        'RES_001',
        '방을 찾을 수 없습니다.',
        HttpStatus.NOT_FOUND,
      );
    }
    if (room.host?.id !== userId) {
      throw new BusinessException(
        'AUTH_003',
        '방 수정 권한이 없습니다.',
        HttpStatus.FORBIDDEN,
      );
    }
    // 이름은 표시용 라벨이라 중복 검사 없이 변경한다(코드는 불변).
    if (dto.name) room.name = dto.name;
    room.scheduledAt = dto.scheduledAt ? new Date(dto.scheduledAt) : null;
    room.allowedUserIds = dto.allowedUserIds?.length
      ? dto.allowedUserIds
      : null;
    await this.roomRepo.save(room);
    return this.toRoomResponse(room, userId);
  }

  // 방 삭제 — 진행자(생성자)만 가능.
  async deleteRoom(userId: string, roomId: string): Promise<void> {
    const room = await this.roomRepo.findOne({
      where: { id: roomId },
      relations: ['host'],
    });
    if (!room) {
      throw new BusinessException(
        'RES_001',
        '방을 찾을 수 없습니다.',
        HttpStatus.NOT_FOUND,
      );
    }
    if (room.host?.id !== userId) {
      throw new BusinessException(
        'AUTH_003',
        '방 삭제 권한이 없습니다.',
        HttpStatus.FORBIDDEN,
      );
    }
    await this.roomRepo.remove(room);
  }

  private isRestricted(room: MeetingRoom): boolean {
    return !!(room.allowedUserIds && room.allowedUserIds.length > 0);
  }

  private async toRoomResponse(
    room: MeetingRoom,
    userId: string,
  ): Promise<MeetingRoomResponse> {
    const isHost = room.host?.id === userId;
    const restricted = this.isRestricted(room);
    const canJoin =
      isHost || !restricted || room.allowedUserIds!.includes(userId);
    return {
      id: room.id,
      code: room.code,
      name: room.name,
      host: room.host ? UserService.toPublic(room.host) : null,
      scheduledAt: room.scheduledAt ? room.scheduledAt.toISOString() : null,
      restricted,
      isHost,
      canJoin,
      // 편집 폼 사전 채움을 위해 진행자에게만 허용 멤버를 풀어서 내려준다(저장 순서 보존).
      allowedUsers:
        isHost && restricted
          ? await this.resolveAllowedUsers(room.allowedUserIds!)
          : null,
      createdAt: room.createdAt.toISOString(),
    };
  }

  // 허용 사용자 ID 목록을 PublicUser 로 해석(저장된 순서 유지, 삭제된 사용자는 제외).
  private async resolveAllowedUsers(ids: string[]): Promise<PublicUser[]> {
    if (!ids.length) return [];
    const users = await this.userRepo.findBy({ id: In(ids) });
    const byId = new Map(users.map((u) => [u.id, u]));
    return ids
      .map((id) => byId.get(id))
      .filter((u): u is User => !!u)
      .map((u) => UserService.toPublic(u));
  }
}
