import {
  BadRequestException,
  ForbiddenException,
  Injectable,
  Logger,
  NotFoundException,
} from '@nestjs/common';
import { InjectDataSource } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';

import { Meeting, MeetingStatus, MeetingType } from '../entities/meeting.entity';
import {
  JoinChannel,
  MeetingParticipant,
  ParticipantRole,
} from '../entities/meeting-participant.entity';
import { AuditScheduleRow } from '../../audit-schedules/entities/audit-schedule-row.entity';
import { User } from '../../user/entities/user.entity';

import { CreateMeetingDto } from '../dto/create-meeting.dto';
import { UpdateMeetingDto } from '../dto/update-meeting.dto';
import { ListMeetingsQueryDto } from '../dto/list-meetings.dto';
import { EndMeetingDto } from '../dto/end-meeting.dto';
import { RoomCodeGeneratorService } from './room-code-generator.service';

const SUPER_ADMIN_IDS = [1, 8];

export interface AttendanceRow {
  userId: string | number | null;
  displayName: string;
  email?: string | null;
  role: string;
  channel?: string;
  joinedAt: number;
  leftAt: number;
}

@Injectable()
export class MeetingService {
  private readonly logger = new Logger('MeetingService');

  constructor(
    @InjectDataSource('scheme_dbs')
    private readonly dataSource: DataSource,
    private readonly codeGenerator: RoomCodeGeneratorService,
  ) {}

  // ═══════════════════════════════════════════════════════════════════════
  // CREATE
  // ═══════════════════════════════════════════════════════════════════════

  async create(dto: CreateMeetingDto, currentUserId: number): Promise<Meeting> {
    const id = await this.dataSource.transaction(async (manager) => {
      let type = dto.type ?? MeetingType.OTHER;
      let companyId = dto.company_id ?? null;

      // An audit link decides the type and the company — never trust the
      // client to send a company that disagrees with the audit.
      if (dto.audit_row_id) {
        const auditRow = await manager.findOne(AuditScheduleRow, {
          where: { id: dto.audit_row_id },
          relations: ['company', 'schedule'],
        });
        if (!auditRow) {
          throw new NotFoundException('Audit row not found');
        }
        type = MeetingType.AUDIT;
        companyId = auditRow.company_id ?? null;
      }

      const room_code = await this.codeGenerator.generate(manager);

      const meeting = manager.create(Meeting, {
        room_code,
        title: dto.title,
        description: dto.description ?? null,
        type,
        status: MeetingStatus.SCHEDULED,
        audit_row_id: dto.audit_row_id ?? null,
        company_id: companyId,
        host_user_id: currentUserId,
        scheduled_at: dto.scheduled_at ? new Date(dto.scheduled_at) : null,
        is_recorded: dto.is_recorded ?? true,
        created_by_id: currentUserId,
      });

      const saved = await manager.save(meeting);
      return saved.id;
    });

    this.logger.log(`Meeting created (id=${id}, host=${currentUserId})`);
    return this.findOne(id);
  }

  /**
   * Convenience for the audit detail page: one call that finds an existing
   * open meeting for this audit or creates one. Stops a coordinator ending up
   * with five half-used meetings for the same audit.
   */
  async createForAudit(
    auditRowId: number,
    currentUserId: number,
  ): Promise<Meeting> {
    const existing = await this.dataSource.getRepository(Meeting).findOne({
      where: [
        { audit_row_id: auditRowId, status: MeetingStatus.SCHEDULED },
        { audit_row_id: auditRowId, status: MeetingStatus.LIVE },
      ],
      order: { created_at: 'DESC' },
    });
    if (existing) return this.findOne(existing.id);

    const row = await this.dataSource.getRepository(AuditScheduleRow).findOne({
      where: { id: auditRowId },
      relations: ['company', 'schedule', 'standards'],
    });
    if (!row) throw new NotFoundException('Audit row not found');

    const standards =
      (row as any).standards?.map((s: any) => s.name).join(', ') || 'Audit';
    const title = `${row.company?.name ?? 'Audit'} — ${row.audit_code}`;

    return this.create(
      {
        title,
        description:
          `${row.audit_type} · ${row.audit_stage ?? ''} · ${standards}`.trim(),
        audit_row_id: auditRowId,
        scheduled_at: row.schedule?.schedule_date
          ? new Date(
              `${row.schedule.schedule_date}T${(row as any).audit_time ?? '09:00:00'}`,
            ).toISOString()
          : undefined,
      },
      currentUserId,
    );
  }

  // ═══════════════════════════════════════════════════════════════════════
  // READ
  // ═══════════════════════════════════════════════════════════════════════

  async findOne(id: number): Promise<Meeting> {
    const meeting = await this.dataSource
      .getRepository(Meeting)
      .createQueryBuilder('m')
      .leftJoinAndSelect('m.company', 'company')
      .leftJoinAndSelect('m.host_user', 'host_user')
      .leftJoinAndSelect('m.audit_row', 'audit_row')
      .leftJoinAndSelect('audit_row.schedule', 'schedule')
      .leftJoinAndSelect('audit_row.standards', 'standards')
      .leftJoinAndSelect('m.participants', 'participants')
      .where('m.id = :id', { id })
      .getOne();

    if (!meeting) throw new NotFoundException('Meeting not found');
    return meeting;
  }

  async findByRoomCode(roomCode: string): Promise<Meeting> {
    const meeting = await this.dataSource
      .getRepository(Meeting)
      .createQueryBuilder('m')
      .leftJoinAndSelect('m.company', 'company')
      .leftJoinAndSelect('m.host_user', 'host_user')
      .leftJoinAndSelect('m.audit_row', 'audit_row')
      .leftJoinAndSelect('audit_row.standards', 'standards')
      .where('m.room_code = :roomCode', { roomCode })
      .getOne();

    if (!meeting) throw new NotFoundException('Meeting not found');
    return meeting;
  }

  async findAll(q: ListMeetingsQueryDto, currentUserId: number) {
    const page = q.page || 1;
    const limit = q.limit || 25;
    const skip = (page - 1) * limit;

    const qb = this.dataSource
      .getRepository(Meeting)
      .createQueryBuilder('m')
      .leftJoinAndSelect('m.company', 'company')
      .leftJoinAndSelect('m.host_user', 'host_user')
      .leftJoinAndSelect('m.audit_row', 'audit_row')
      .orderBy('m.scheduled_at', 'DESC')
      .addOrderBy('m.id', 'DESC')
      .skip(skip)
      .take(limit);

    // Anyone without view-all sees only meetings they host or attended.
    const canViewAll = await this.userCanViewAll(currentUserId);
    if (!canViewAll || q.mine === 'true') {
      qb.andWhere(
        `(m.host_user_id = :uid OR EXISTS (
            SELECT 1 FROM meeting_participants mp
            WHERE mp.meeting_id = m.id AND mp.user_id = :uid
          ))`,
        { uid: currentUserId },
      );
    }

    if (q.status) qb.andWhere('m.status = :status', { status: q.status });
    if (q.type) qb.andWhere('m.type = :type', { type: q.type });
    if (q.audit_row_id)
      qb.andWhere('m.audit_row_id = :arid', { arid: q.audit_row_id });
    if (q.company_id) qb.andWhere('m.company_id = :cid', { cid: q.company_id });
    if (q.date_from) qb.andWhere('m.scheduled_at >= :df', { df: q.date_from });
    if (q.date_to) qb.andWhere('m.scheduled_at <= :dt', { dt: q.date_to });
    if (q.search) {
      qb.andWhere(
        '(m.title LIKE :s OR m.room_code LIKE :s OR company.name LIKE :s)',
        { s: `%${q.search}%` },
      );
    }

    const [data, total] = await qb.getManyAndCount();

    return {
      data,
      meta: { total, page, limit, totalPages: Math.ceil(total / limit) },
    };
  }

  /** Every meeting ever held for an audit — the history your PDF promises */
  async findByAudit(auditRowId: number) {
    return this.dataSource.getRepository(Meeting).find({
      where: { audit_row_id: auditRowId },
      relations: ['participants', 'host_user'],
      order: { created_at: 'DESC' },
    });
  }

  // ═══════════════════════════════════════════════════════════════════════
  // UPDATE / LIFECYCLE
  // ═══════════════════════════════════════════════════════════════════════

  async update(
    id: number,
    dto: UpdateMeetingDto,
    currentUserId: number,
  ): Promise<Meeting> {
    const repo = this.dataSource.getRepository(Meeting);
    const meeting = await repo.findOne({ where: { id } });
    if (!meeting) throw new NotFoundException('Meeting not found');

    await this.assertCanManage(meeting, currentUserId);

    if (meeting.status === MeetingStatus.ENDED) {
      throw new BadRequestException('Cannot edit a meeting that has ended');
    }

    Object.assign(meeting, {
      title: dto.title ?? meeting.title,
      description: dto.description ?? meeting.description,
      scheduled_at: dto.scheduled_at
        ? new Date(dto.scheduled_at)
        : meeting.scheduled_at,
      is_recorded: dto.is_recorded ?? meeting.is_recorded,
      status: dto.status ?? meeting.status,
    });

    await repo.save(meeting);
    return this.findOne(id);
  }

  /** Called by the gateway when the first participant joins */
  async markLive(roomCode: string): Promise<void> {
    const repo = this.dataSource.getRepository(Meeting);
    const meeting = await repo.findOne({ where: { room_code: roomCode } });
    if (!meeting || meeting.status === MeetingStatus.LIVE) return;

    meeting.status = MeetingStatus.LIVE;
    meeting.started_at = meeting.started_at ?? new Date();
    await repo.save(meeting);
    this.logger.log(`Meeting live: ${roomCode}`);
  }

  /**
   * Persist the attendance the gateway has been holding in memory.
   *
   * RoomRegistry computes join and leave times per person and then discards
   * them. This is what turns that into the IAF MD 4 evidence record.
   */
  async persistAttendance(
    roomCode: string,
    rows: AttendanceRow[],
  ): Promise<void> {
    if (!rows?.length) return;

    await this.dataSource.transaction(async (manager) => {
      const meeting = await manager.findOne(Meeting, {
        where: { room_code: roomCode },
      });
      if (!meeting) {
        this.logger.warn(`persistAttendance: no meeting for ${roomCode}`);
        return;
      }

      for (const r of rows) {
        const joined = new Date(r.joinedAt);
        const left = new Date(r.leftAt);
        const seconds = Math.max(
          0,
          Math.round((left.getTime() - joined.getTime()) / 1000),
        );

        const numericUserId =
          r.userId !== null &&
          r.userId !== undefined &&
          !isNaN(Number(r.userId))
            ? Number(r.userId)
            : null;

        await manager.save(
          manager.create(MeetingParticipant, {
            meeting_id: meeting.id,
            user_id: numericUserId,
            display_name: r.displayName,
            email: r.email ?? null,
            role: this.mapRole(r.role),
            channel: this.mapChannel(r.channel),
            joined_at: joined,
            left_at: left,
            duration_seconds: seconds,
          }),
        );
      }

      this.logger.log(
        `Attendance saved: ${roomCode} — ${rows.length} participant record(s)`,
      );
    });
  }

  async end(
    id: number,
    dto: EndMeetingDto,
    currentUserId: number,
  ): Promise<Meeting> {
    const repo = this.dataSource.getRepository(Meeting);
    const meeting = await repo.findOne({ where: { id } });
    if (!meeting) throw new NotFoundException('Meeting not found');

    await this.assertCanManage(meeting, currentUserId);

    if (meeting.status === MeetingStatus.ENDED) return this.findOne(id);

    const now = new Date();
    meeting.status = MeetingStatus.ENDED;
    meeting.ended_at = now;
    meeting.duration_seconds = meeting.started_at
      ? Math.round((now.getTime() - meeting.started_at.getTime()) / 1000)
      : 0;
    if (dto?.summary) meeting.summary = dto.summary;

    await repo.save(meeting);
    this.logger.log(`Meeting ended (id=${id}, ${meeting.duration_seconds}s)`);

    return this.findOne(id);
  }

  async cancel(id: number, currentUserId: number): Promise<Meeting> {
    const repo = this.dataSource.getRepository(Meeting);
    const meeting = await repo.findOne({ where: { id } });
    if (!meeting) throw new NotFoundException('Meeting not found');

    await this.assertCanManage(meeting, currentUserId);

    if (meeting.status === MeetingStatus.ENDED) {
      throw new BadRequestException('Cannot cancel a meeting that has ended');
    }

    meeting.status = MeetingStatus.CANCELLED;
    await repo.save(meeting);
    return this.findOne(id);
  }

  // ═══════════════════════════════════════════════════════════════════════
  // ACCESS
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * Can this user join this room?
   *
   * Used by the gateway's `join` handler. Without it, any logged-in user who
   * knows a room code can walk into a confidential audit.
   */
  async canJoin(userId: number, roomCode: string): Promise<boolean> {
    const meeting = await this.dataSource.getRepository(Meeting).findOne({
      where: { room_code: roomCode },
      relations: ['audit_row', 'audit_row.co_auditors'],
    });
    if (!meeting) return false;
    if (meeting.status === MeetingStatus.CANCELLED) return false;

    if (meeting.host_user_id === userId) return true;
    if (SUPER_ADMIN_IDS.includes(userId)) return true;

    // assigned to the audit this meeting belongs to
    if (meeting.audit_row) {
      if ((meeting.audit_row as any).lead_auditor_id === userId) return true;
      const coIds = (meeting.audit_row as any).co_auditors?.map(
        (u: User) => u.id,
      );
      if (coIds?.includes(userId)) return true;
    }

    // already attended — covers reconnects and people who joined by invite
    const seen = await this.dataSource
      .getRepository(MeetingParticipant)
      .findOne({ where: { meeting_id: meeting.id, user_id: userId } });
    if (seen) return true;

    return this.userCanViewAll(userId);
  }

  private async assertCanManage(
    meeting: Meeting,
    userId: number,
  ): Promise<void> {
    if (meeting.host_user_id === userId) return;
    if (SUPER_ADMIN_IDS.includes(userId)) return;
    const canViewAll = await this.userCanViewAll(userId);
    if (canViewAll) return;
    throw new ForbiddenException('You can only manage meetings you host');
  }

  /** Mirrors the permission pattern used across the rest of the app. */
  private async userCanViewAll(userId: number): Promise<boolean> {
    if (SUPER_ADMIN_IDS.includes(userId)) return true;

    const rolePerms = await this.dataSource.query(
      `
      SELECT 1
      FROM users u
      INNER JOIN user_roles ur ON ur.user_id = u.id
      INNER JOIN role_permissions rp ON rp.role_id = ur.role_id
      INNER JOIN permissions p ON p.id = rp.permission_id
      INNER JOIN modules m ON m.id = p.module_id
      WHERE u.id = ?
        AND p.action = 'view-all'
        AND m.slug = 'meetings'
      LIMIT 1
      `,
      [userId],
    );
    if (rolePerms.length > 0) return true;

    try {
      const directPerms = await this.dataSource.query(
        `
        SELECT 1
        FROM user_permissions up
        INNER JOIN permissions p ON p.id = up.permission_id
        INNER JOIN modules m ON m.id = p.module_id
        WHERE up.user_id = ?
          AND p.action = 'view-all'
          AND m.slug = 'meetings'
        LIMIT 1
        `,
        [userId],
      );
      if (directPerms.length > 0) return true;
    } catch {
      /* user_permissions may not exist — fine */
    }

    return false;
  }

  private mapRole(role?: string): ParticipantRole {
    switch (String(role || '').toUpperCase()) {
      case 'AUDITOR':
        return ParticipantRole.AUDITOR;
      case 'CLIENT':
        return ParticipantRole.CLIENT;
      case 'HOST':
        return ParticipantRole.HOST;
      default:
        return ParticipantRole.OBSERVER;
    }
  }

  private mapChannel(channel?: string): JoinChannel {
    switch (String(channel || '').toUpperCase()) {
      case 'BROWSER':
        return JoinChannel.BROWSER;
      case 'PHONE':
        return JoinChannel.PHONE;
      default:
        return JoinChannel.APP;
    }
  }
}
