import { fn, col, Op } from 'sequelize';
import db from '@/db/models';
import { withTransaction } from '@/db/with-transaction';
import { requiredIsoDate } from '@/services/shared/validate';
import ForbiddenError from '@/shared/errors/forbidden';
import ValidationError from '@/shared/errors/validation';
import { ROLE_SCOPES } from '@/shared/constants/roles';
import {
  assertAuthenticatedTenantUser,
  getDisplayName,
  getRoleScope,
  getOrganizationId,
  getSchoolId,
  getCampusId,
  getClassId,
  hasGlobalAccess,
  requireUserId,
} from '@/services/shared/access';
import type { ClassAttendance } from '@/db/models/class_attendance';
import type { CurrentUser } from '@/db/api/types';

interface ClassAttendanceInput {
  total_enrolled?: unknown;
  total_present?: unknown;
  total_absent?: unknown;
  total_tardy?: unknown;
}

interface SummaryFilter {
  startDate?: unknown;
  endDate?: unknown;
}

function requiredNonNegativeInteger(value: unknown): number {
  if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {
    throw new ValidationError();
  }
  return value;
}

function validateTotals(data: ClassAttendanceInput) {
  const totalEnrolled = requiredNonNegativeInteger(data.total_enrolled);
  const totalPresent = requiredNonNegativeInteger(data.total_present);
  const totalAbsent = requiredNonNegativeInteger(data.total_absent);
  const totalTardy = requiredNonNegativeInteger(data.total_tardy ?? 0);

  if (
    totalEnrolled <= 0 ||
    totalPresent > totalEnrolled ||
    totalAbsent > totalEnrolled ||
    totalTardy > totalEnrolled
  ) {
    throw new ValidationError();
  }

  return {
    total_enrolled: totalEnrolled,
    total_present: totalPresent,
    total_absent: totalAbsent,
    total_tardy: totalTardy,
    attendance_percentage: ((totalPresent / totalEnrolled) * 100).toFixed(2),
  };
}

/** Subtree rollup filter for the active tier, over the denormalized columns. */
function rollupScope(currentUser?: CurrentUser): Record<string, unknown> {
  if (hasGlobalAccess(currentUser)) {
    return {};
  }
  const organizationId = getOrganizationId(currentUser);
  const base = organizationId ? { organizationId } : {};
  switch (getRoleScope(currentUser)) {
    case ROLE_SCOPES.SCHOOL:
      return { ...base, schoolId: getSchoolId(currentUser) };
    case ROLE_SCOPES.CAMPUS:
      return { ...base, campusId: getCampusId(currentUser) };
    case ROLE_SCOPES.CLASS:
      return { ...base, classId: getClassId(currentUser) };
    default:
      return base;
  }
}

/** Asserts the user may record attendance for `classData`'s tenant chain. */
function assertCanRecord(
  classData: { organizationId: string | null; campusId: string | null; schoolId: string | null; id: string },
  currentUser?: CurrentUser,
): void {
  if (hasGlobalAccess(currentUser)) {
    return;
  }
  const organizationId = getOrganizationId(currentUser);
  if (organizationId && organizationId !== classData.organizationId) {
    throw new ForbiddenError();
  }
  switch (getRoleScope(currentUser)) {
    case ROLE_SCOPES.ORGANIZATION:
      return;
    case ROLE_SCOPES.SCHOOL:
      if (getSchoolId(currentUser) === classData.schoolId) return;
      break;
    case ROLE_SCOPES.CAMPUS:
      if (getCampusId(currentUser) === classData.campusId) return;
      break;
    case ROLE_SCOPES.CLASS:
      if (getClassId(currentUser) === classData.id) return;
      break;
    default:
      break;
  }
  throw new ForbiddenError();
}

function dateRange(filter: SummaryFilter): Record<string, unknown> {
  const start = filter.startDate ? requiredIsoDate(filter.startDate) : null;
  const end = filter.endDate ? requiredIsoDate(filter.endDate) : null;
  if (!start && !end) return {};
  return {
    attendance_date: {
      ...(start ? { [Op.gte]: start } : {}),
      ...(end ? { [Op.lte]: end } : {}),
    },
  };
}

function toDto(record: ClassAttendance) {
  const plain = record.get({ plain: true });
  return {
    id: plain.id,
    classId: plain.classId,
    date: plain.attendance_date,
    total_enrolled: plain.total_enrolled,
    total_present: plain.total_present,
    total_absent: plain.total_absent,
    total_tardy: plain.total_tardy,
    attendance_percentage: Number(plain.attendance_percentage),
    recorded_by_label: plain.recorded_by_label,
    organizationId: plain.organizationId,
    schoolId: plain.schoolId,
    campusId: plain.campusId,
  };
}

class ClassAttendanceService {
  /** Teacher (or a higher tier drilling in) records a class's daily aggregate. */
  static async upsert(
    classIdParam: unknown,
    dateParam: unknown,
    data: ClassAttendanceInput,
    currentUser?: CurrentUser,
  ) {
    assertAuthenticatedTenantUser(currentUser);

    const classId = typeof classIdParam === 'string' ? classIdParam : '';
    if (!classId) {
      throw new ValidationError();
    }
    const attendanceDate = requiredIsoDate(dateParam);

    const cls = await db.classes.findByPk(classId);
    if (!cls) {
      throw new ValidationError();
    }
    const campus = cls.campusId
      ? await db.campuses.findByPk(cls.campusId)
      : null;
    const classData = {
      id: cls.id,
      organizationId: cls.organizationId ?? null,
      campusId: cls.campusId ?? null,
      schoolId: campus?.schoolId ?? null,
    };

    assertCanRecord(classData, currentUser);

    const totals = validateTotals(data);
    const payload = {
      ...totals,
      classId,
      attendance_date: attendanceDate,
      recorded_by_label: getDisplayName(currentUser),
      organizationId: classData.organizationId,
      schoolId: classData.schoolId,
      campusId: classData.campusId,
      updatedById: currentUser?.id ?? null,
    };

    return withTransaction(async (transaction) => {
      const existing = await db.class_attendance.findOne({
        where: { classId, attendance_date: attendanceDate },
        transaction,
      });
      const saved = existing
        ? await existing.update(payload, { transaction })
        : await db.class_attendance.create(
            { ...payload, createdById: requireUserId(currentUser) },
            { transaction },
          );
      return toDto(saved);
    });
  }

  /** Per-date roll-up totals for the active tier (its whole subtree summed). */
  static async summary(filter: SummaryFilter, currentUser?: CurrentUser) {
    assertAuthenticatedTenantUser(currentUser);

    const rows = await db.class_attendance.findAll({
      attributes: [
        'attendance_date',
        [fn('SUM', col('total_enrolled')), 'total_enrolled'],
        [fn('SUM', col('total_present')), 'total_present'],
        [fn('SUM', col('total_absent')), 'total_absent'],
        [fn('SUM', col('total_tardy')), 'total_tardy'],
      ],
      where: { ...rollupScope(currentUser), ...dateRange(filter) },
      group: ['attendance_date'],
      order: [['attendance_date', 'DESC']],
    });

    return {
      rows: rows.map((row) => {
        const plain = row.get({ plain: true }) as Record<string, unknown>;
        const enrolled = Number(plain.total_enrolled) || 0;
        const present = Number(plain.total_present) || 0;
        return {
          date: plain.attendance_date,
          total_enrolled: enrolled,
          total_present: present,
          total_absent: Number(plain.total_absent) || 0,
          total_tardy: Number(plain.total_tardy) || 0,
          attendance_percentage:
            enrolled > 0 ? Number(((present / enrolled) * 100).toFixed(2)) : 0,
        };
      }),
    };
  }
}

export default ClassAttendanceService;
