import { Op } from 'sequelize';
import db from '@/db/models';
import ValidationError from '@/shared/errors/validation';
import UserProgressService from '@/services/user_progress';
import {
  assertAuthenticatedTenantUser,
  getCampusId,
  getClassId,
  hasFeaturePermission,
  isActingInOwnScope,
  getOrganizationIdOrGlobal,
  getRoleScope,
  getSchoolId,
} from '@/services/shared/access';
import ForbiddenError from '@/shared/errors/forbidden';
import { FEATURE_PERMISSIONS } from '@/shared/constants/product-permissions';
import { ROLE_NAMES, ROLE_SCOPES } from '@/shared/constants/roles';
import { localDateInTimezone } from '@/shared/constants/timezone';
import { USER_PROGRESS_TYPES } from '@/shared/constants/user-progress';
import {
  ZONE_CHECKIN_HISTORY_LIMIT,
  isZoneCheckinColor,
} from '@/shared/constants/zone-checkin';
import type { CurrentUser } from '@/db/api/types';
import type { Users } from '@/db/models/users';
import type { UserProgress } from '@/db/models/user_progress';

/**
 * Daily Zone check-in service (Workstream 16). "Today" is computed server-side
 * in the user's **campus timezone**, so it is independent of the caller's device
 * clock/zone and correct across organizations, campuses, and DST. Storage reuses
 * the per-user `user_progress` store via {@link UserProgressService}; the
 * campus-local date is the `item_id`, so one row exists per user per day and the
 * full set of rows is the history.
 */

export interface ZoneCheckinTodayPayload {
  readonly date: string;
  readonly zone: string | null;
  readonly isCheckedInToday: boolean;
}

export interface ZoneCheckinHistoryEntry {
  readonly date: string;
  readonly zone: string;
}

export interface ZoneCheckinCompletionRow {
  readonly userId: string;
  readonly name: string;
  readonly email: string | null;
  readonly avatar: string | null;
  readonly tenantName: string | null;
  readonly tenantLogo: string | null;
  readonly role: string | null;
  readonly date: string;
  readonly status: 'complete' | 'pending';
  readonly zone: string | null;
  readonly riskLevel: 'none' | 'medium' | 'pending';
  readonly result: string;
}

function assertCanZoneCheckIn(currentUser?: CurrentUser): void {
  if (!hasFeaturePermission(currentUser, FEATURE_PERMISSIONS.ZONE_CHECKIN)) {
    throw new ForbiddenError();
  }
}

function assertCanMutateZoneCheckIn(currentUser?: CurrentUser): void {
  assertCanZoneCheckIn(currentUser);
  if (!isActingInOwnScope(currentUser)) {
    throw new ForbiddenError();
  }
}

function assertCanReadCompletion(currentUser?: CurrentUser): void {
  assertAuthenticatedTenantUser(currentUser);
  if (
    hasFeaturePermission(
      currentUser,
      FEATURE_PERMISSIONS.READ_ZONE_CHECKIN_REPORTS,
    )
  ) {
    return;
  }
  throw new ForbiddenError();
}

const REPORT_STAFF_ROLES = Object.freeze([
  ROLE_NAMES.OWNER,
  ROLE_NAMES.SUPERINTENDENT,
  ROLE_NAMES.PRINCIPAL,
  ROLE_NAMES.REGISTRAR,
  ROLE_NAMES.DIRECTOR,
  ROLE_NAMES.OFFICE_MANAGER,
  ROLE_NAMES.TEACHER,
  ROLE_NAMES.SUPPORT_STAFF,
]);

interface StaffTenantInfo {
  readonly tenantName: string | null;
  readonly tenantLogo: string | null;
}

async function resolveCampusTimezone(currentUser?: CurrentUser): Promise<string> {
  const campusId = getCampusId(currentUser);
  if (!campusId) {
    return 'UTC';
  }
  const campus = await db.campuses.findByPk(campusId, {
    attributes: ['timezone'],
  });
  if (!campus) {
    return 'UTC';
  }
  return campus.timezone;
}

async function staffScopeWhere(currentUser?: CurrentUser) {
  const organizationId = getOrganizationIdOrGlobal(currentUser);
  const base = organizationId ? { organizationId } : {};
  const scope = getRoleScope(currentUser);

  if (scope === ROLE_SCOPES.SCHOOL) {
    const schoolId = getSchoolId(currentUser);
    if (!schoolId) {
      return base;
    }
    const campuses = await db.campuses.findAll({
      attributes: ['id'],
      where: {
        schoolId,
        ...(organizationId ? { organizationId } : {}),
      },
    });
    const campusIds = campuses.map((campus) => campus.id);
    const classes = campusIds.length > 0
      ? await db.classes.findAll({
          attributes: ['id'],
          where: {
            campusId: campusIds,
            ...(organizationId ? { organizationId } : {}),
          },
        })
      : [];
    const classIds = classes.map((classroom) => classroom.id);

    return {
      ...base,
      [Op.or]: [
        { schoolId },
        ...(campusIds.length > 0 ? [{ campusId: { [Op.in]: campusIds } }] : []),
        ...(classIds.length > 0 ? [{ classId: { [Op.in]: classIds } }] : []),
      ],
    };
  }

  if (scope === ROLE_SCOPES.CAMPUS) {
    const campusId = getCampusId(currentUser);
    return campusId ? { ...base, campusId } : base;
  }

  if (scope === ROLE_SCOPES.CLASS) {
    const classId = getClassId(currentUser);
    return classId ? { ...base, classId } : base;
  }

  return base;
}

function displayNameOf(user: Users): string {
  return [user.firstName, user.lastName].filter(Boolean).join(' ').trim()
    || user.email
    || 'Staff Member';
}

function avatarOf(user: Users): string | null {
  return user.avatar?.[0]?.privateUrl ?? null;
}

function tenantOf(user: Users): StaffTenantInfo {
  const tenant = user.class ?? user.campus ?? user.school ?? user.organizations ?? null;
  return {
    tenantName: tenant?.name ?? null,
    tenantLogo: tenant?.logo ?? null,
  };
}

function latestProgressByUserAndDate(
  rows: readonly UserProgress[],
): ReadonlyMap<string, UserProgress> {
  const byUserAndDate = new Map<string, UserProgress>();
  for (const row of rows) {
    const key = `${row.userId}:${row.item_id}`;
    if (byUserAndDate.has(key)) {
      continue;
    }
    byUserAndDate.set(key, row);
  }
  return byUserAndDate;
}

class ZoneCheckinService {
  /** Today's check-in for the caller (campus-local date). */
  static async today(currentUser?: CurrentUser): Promise<ZoneCheckinTodayPayload> {
    assertAuthenticatedTenantUser(currentUser);
    assertCanZoneCheckIn(currentUser);
    const timezone = await resolveCampusTimezone(currentUser);
    const date = localDateInTimezone(timezone);

    const { rows } = await UserProgressService.list(
      { progress_type: USER_PROGRESS_TYPES.ZONE_CHECKIN, item_id: date },
      currentUser,
    );
    const zone = rows[0]?.value ?? null;
    return { date, zone, isCheckedInToday: zone !== null };
  }

  /** Record (upsert) today's zone for the caller. */
  static async checkIn(
    data: { zone?: unknown },
    currentUser?: CurrentUser,
  ): Promise<ZoneCheckinTodayPayload> {
    assertAuthenticatedTenantUser(currentUser);
    assertCanMutateZoneCheckIn(currentUser);
    if (!isZoneCheckinColor(data.zone)) {
      throw new ValidationError('zoneCheckinInvalidZone');
    }
    const timezone = await resolveCampusTimezone(currentUser);
    const date = localDateInTimezone(timezone);

    await UserProgressService.upsert(
      {
        progress_type: USER_PROGRESS_TYPES.ZONE_CHECKIN,
        item_id: date,
        value: data.zone,
      },
      currentUser,
    );
    return { date, zone: data.zone, isCheckedInToday: true };
  }

  /** Clears the caller's check-in for today (campus-local date). */
  static async clearToday(currentUser?: CurrentUser): Promise<ZoneCheckinTodayPayload> {
    assertAuthenticatedTenantUser(currentUser);
    assertCanMutateZoneCheckIn(currentUser);
    const timezone = await resolveCampusTimezone(currentUser);
    const date = localDateInTimezone(timezone);

    await UserProgressService.removeByItem(
      { progress_type: USER_PROGRESS_TYPES.ZONE_CHECKIN, item_id: date },
      currentUser,
    );
    return { date, zone: null, isCheckedInToday: false };
  }

  /** The caller's check-in history (optionally within a [from, to] date range). */
  static async history(
    filter: { from?: unknown; to?: unknown },
    currentUser?: CurrentUser,
  ): Promise<{ rows: ZoneCheckinHistoryEntry[]; count: number }> {
    assertAuthenticatedTenantUser(currentUser);
    assertCanZoneCheckIn(currentUser);
    const { rows } = await UserProgressService.list(
      {
        progress_type: USER_PROGRESS_TYPES.ZONE_CHECKIN,
        limit: ZONE_CHECKIN_HISTORY_LIMIT,
      },
      currentUser,
    );

    const from = typeof filter.from === 'string' ? filter.from : null;
    const to = typeof filter.to === 'string' ? filter.to : null;

    // `item_id` is the YYYY-MM-DD date, so string comparison is chronological.
    const entries = rows
      .filter((row): row is typeof row & { value: string } => row.value !== null)
      .map((row) => ({ date: row.item_id, zone: row.value }))
      .filter((entry) => (!from || entry.date >= from) && (!to || entry.date <= to))
      .sort((a, b) => (a.date < b.date ? 1 : -1));

    return { rows: entries, count: entries.length };
  }

  /** Staff completion report for today's check-in across the caller's scope. */
  static async completion(currentUser?: CurrentUser) {
    assertCanReadCompletion(currentUser);
    const staffUsers = await db.users.findAll({
      where: {
        disabled: false,
        ...(await staffScopeWhere(currentUser)),
      },
      include: [
        {
          model: db.roles,
          as: 'app_role',
          required: true,
          where: { name: REPORT_STAFF_ROLES },
        },
        {
          model: db.file,
          as: 'avatar',
          required: false,
        },
        {
          model: db.organizations,
          as: 'organizations',
          required: false,
          attributes: ['id', 'name', 'logo'],
        },
        {
          model: db.schools,
          as: 'school',
          required: false,
          attributes: ['id', 'name', 'logo'],
        },
        {
          model: db.campuses,
          as: 'campus',
          required: false,
          attributes: ['id', 'name', 'logo'],
        },
        {
          model: db.classes,
          as: 'class',
          required: false,
          attributes: ['id', 'name', 'logo'],
        },
      ],
      order: [
        ['lastName', 'asc'],
        ['firstName', 'asc'],
        ['email', 'asc'],
      ],
    });
    const campusIds = [...new Set(staffUsers.map((user) => user.campusId).filter((id): id is string => Boolean(id)))];
    const campuses = campusIds.length > 0
      ? await db.campuses.findAll({
          attributes: ['id', 'timezone'],
          where: { id: { [Op.in]: campusIds } },
        })
      : [];
    const timezoneByCampus = new Map(campuses.map((campus) => [campus.id, campus.timezone]));
    const dateByUser = new Map(
      staffUsers.map((user) => {
        const timezone = user.campusId ? timezoneByCampus.get(user.campusId) : null;
        return [user.id, localDateInTimezone(timezone ?? 'UTC')] as const;
      }),
    );
    const userIds = staffUsers.map((user) => user.id);
    const dates = [...new Set(dateByUser.values())];
    const organizationId = getOrganizationIdOrGlobal(currentUser);
    const progressRows = userIds.length > 0 && dates.length > 0
      ? await db.user_progress.findAll({
          where: {
            ...(organizationId ? { organizationId } : {}),
            userId: { [Op.in]: userIds },
            progress_type: USER_PROGRESS_TYPES.ZONE_CHECKIN,
            item_id: { [Op.in]: dates },
          },
          order: [['updatedAt', 'desc']],
        })
      : [];
    const progressByUserAndDate = latestProgressByUserAndDate(progressRows);
    const rows: ZoneCheckinCompletionRow[] = staffUsers.map((user) => {
      const date = dateByUser.get(user.id) ?? localDateInTimezone('UTC');
      const progress = progressByUserAndDate.get(`${user.id}:${date}`);
      const zone = progress?.value ?? null;
      const status = zone ? 'complete' : 'pending';
      const riskLevel = !zone ? 'pending' : zone === 'green' ? 'none' : 'medium';
      const tenant = tenantOf(user);

      return {
        userId: user.id,
        name: displayNameOf(user),
        email: user.email,
        avatar: avatarOf(user),
        tenantName: tenant.tenantName,
        tenantLogo: tenant.tenantLogo,
        role: user.app_role?.name ?? null,
        date,
        status,
        zone,
        riskLevel,
        result: zone ? `${zone[0].toUpperCase()}${zone.slice(1)} Zone` : 'Pending',
      };
    });
    const completedCount = rows.filter((row) => row.status === 'complete').length;
    const greenCount = rows.filter((row) => row.zone === 'green').length;
    const nonGreenCount = rows.filter((row) => row.zone && row.zone !== 'green').length;

    return {
      summary: {
        totalStaff: rows.length,
        completedCount,
        pendingCount: Math.max(rows.length - completedCount, 0),
        greenCount,
        nonGreenCount,
        completionRate: rows.length > 0
          ? Math.round((completedCount / rows.length) * 100)
          : 0,
      },
      rows,
    };
  }
}

export default ZoneCheckinService;
