import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Student } from '../entities/student.entity';
import { ClassStudent } from '../entities/class-student.entity';
import type { StudentAccessScope } from './student-access-scope';

@Injectable()
export class StudentsAgentService {
  /**
   * Whitelisted output type for agent student searches.
   * NEVER exposes phone, idNumber, emergencyContact, or emergencyPhone.
   */
  private static readonly AGENT_STUDENT_SELECT = [
    'student.id',
    'student.name',
    'student.studentNo',
    'student.gender',
    'student.status',
    'student.organizationId',
    'organization.name',
  ] as const;

  constructor(
    @InjectRepository(Student) private readonly repo: Repository<Student>,
    @InjectRepository(ClassStudent)
    private readonly classStudentRepo: Repository<ClassStudent>,
  ) {}

  /**
   * Search students with SQL-enforced scope, field whitelist, and limit.
   *
   * @param scope — data-range discriminator (manageAll or teacher).
   * @param query — optional keyword, classId, organizationId, limit.
   * @returns formatted whitelist-only results with classIds.
   */
  async agentSearchStudents(
    scope: StudentAccessScope,
    query?: {
      keyword?: string;
      classId?: number;
      organizationId?: number;
      limit?: number;
    },
  ): Promise<
    {
      id: number;
      name: string;
      studentNo: string;
      gender: string;
      status: string;
      organizationId: number;
      organizationName: string;
      classIds: number[];
    }[]
  > {
    const limit = Math.max(1, Math.min(query?.limit ?? 20, 50));

    const qb = this.repo
      .createQueryBuilder('student')
      .distinct(true)
      .select([
        'student.id',
        'student.name',
        'student.studentNo',
        'student.gender',
        'student.status',
        'student.organizationId',
        'student.createdAt',
        'organization.name',
      ])
      .leftJoin('student.organization', 'organization');

    this.applyStudentScope(qb, scope, query?.classId);

    if (query?.keyword) {
      qb.andWhere('(student.name LIKE :keyword OR student.student_no LIKE :keyword)', {
        keyword: `%${query.keyword}%`,
      });
    }
    if (query?.organizationId) {
      qb.andWhere('student.organization_id = :orgId', { orgId: query.organizationId });
    }

    qb.orderBy('student.createdAt', 'DESC').take(limit);

    const rows: Record<string, unknown>[] = await qb.getRawMany();
    if (rows.length === 0) return [];

    // Second bounded query: classIds only for the returned student ids.
    // For teacher scope, the class filter MUST be re-applied so the
    // teacher only sees classIds they are assigned to.
    const studentIds = rows.map((r) => r.student_id as number);
    const csQb = this.classStudentRepo
      .createQueryBuilder('cs')
      .select(['cs.studentId', 'cs.classId'])
      .where('cs.student_id IN (:...ids)', { ids: studentIds })
      .andWhere('cs.status = :status', { status: 'active' });

    if (scope.type === 'teacher') {
      csQb.andWhere(
        'cs.class_id IN (SELECT ct.class_id FROM class_teacher ct WHERE ct.user_id = :scopeTeacherUserId)',
        { scopeTeacherUserId: scope.userId },
      );
    }

    const classRows = await csQb.getRawMany();

    const classMap = new Map<number, number[]>();
    for (const cr of classRows as { cs_student_id: number; cs_class_id: number }[]) {
      const sid = cr.cs_student_id;
      if (!classMap.has(sid)) classMap.set(sid, []);
      classMap.get(sid)!.push(cr.cs_class_id);
    }

    return rows.map((r) => ({
      id: r.student_id as number,
      name: r.student_name as string,
      studentNo: (r.student_student_no as string) ?? '',
      gender: (r.student_gender as string) ?? '',
      status: r.student_status as string,
      organizationId: r.student_organization_id as number,
      organizationName: (r.organization_name as string) ?? '',
      classIds: classMap.get(r.student_id as number) ?? [],
    }));
  }

  /**
   * Get single student basic info with SQL-enforced scope + whitelist.
   * Returns `null` for students out of scope or non-existent (no leak).
   */
  async agentGetStudentBasic(
    scope: StudentAccessScope,
    studentId: number,
  ): Promise<{
    id: number;
    name: string;
    studentNo: string;
    gender: string;
    status: string;
    organizationId: number;
    organizationName: string;
    classIds: number[];
  } | null> {
    const qb = this.repo
      .createQueryBuilder('student')
      .select([
        'student.id',
        'student.name',
        'student.studentNo',
        'student.gender',
        'student.status',
        'student.organizationId',
        'organization.name',
      ])
      .leftJoin('student.organization', 'organization')
      .where('student.id = :studentId', { studentId });

    this.applyStudentScope(qb, scope);

    const row = await qb.getRawOne();
    if (!row) return null;

    // For teacher scope, re-apply class filter so teacher only sees
    // classIds they are assigned to (not ALL active classIds of the student).
    const csQb = this.classStudentRepo
      .createQueryBuilder('cs')
      .select(['cs.classId'])
      .where('cs.student_id = :studentId', { studentId })
      .andWhere('cs.status = :status', { status: 'active' });

    if (scope.type === 'teacher') {
      csQb.andWhere(
        'cs.class_id IN (SELECT ct.class_id FROM class_teacher ct WHERE ct.user_id = :scopeTeacherUserId)',
        { scopeTeacherUserId: scope.userId },
      );
    }

    const classRows = await csQb.getRawMany();

    return {
      id: row.student_id as number,
      name: row.student_name as string,
      studentNo: (row.student_student_no as string) ?? '',
      gender: (row.student_gender as string) ?? '',
      status: row.student_status as string,
      organizationId: row.student_organization_id as number,
      organizationName: (row.organization_name as string) ?? '',
      classIds: (classRows as { cs_class_id: number }[]).map((cr) => cr.cs_class_id),
    };
  }

  /**
   * Apply data-range scope to a student QueryBuilder.
   *
   * - `manageAll`: no restriction.
   * - `teacher`: INNER JOIN ClassStudent → active students in the
   *    teacher's assigned classes (via ClassTeacher).
   * - When `classId` is provided, it is ANDed with the scope
   *   (intersection) — the model cannot widen access.
   */
  private applyStudentScope(
    qb: ReturnType<typeof this.repo.createQueryBuilder>,
    scope: StudentAccessScope,
    classId?: number,
  ): void {
    if (scope.type === 'manageAll') {
      if (classId != null) {
        qb.innerJoin(
          'class_student',
          'cs_scope',
          'cs_scope.student_id = student.id AND cs_scope.class_id = :scopeClassId AND cs_scope.status = :scopeCsStatus',
          { scopeClassId: classId, scopeCsStatus: 'active' },
        );
      }
      return;
    }

    // Teacher scope: active students in teacher's assigned classes
    const teacherClause =
      'cs_scope.student_id = student.id AND cs_scope.status = :scopeCsStatus AND cs_scope.class_id IN ' +
      '(SELECT ct.class_id FROM class_teacher ct WHERE ct.user_id = :scopeTeacherUserId)';

    qb.innerJoin('class_student', 'cs_scope', teacherClause, {
      scopeTeacherUserId: scope.userId,
      scopeCsStatus: 'active',
    });

    if (classId != null) {
      qb.andWhere('cs_scope.class_id = :scopeClassId', { scopeClassId: classId });
    }
  }
}
