import db from '@/db/models';
import ValidationError from '@/shared/errors/validation';
import UserProgressService from '@/services/user_progress';
import {
  assertAuthenticatedTenantUser,
  getCampusId,
  hasFeaturePermission,
  isActingInOwnScope,
} from '@/services/shared/access';
import ForbiddenError from '@/shared/errors/forbidden';
import { FEATURE_PERMISSIONS } from '@/shared/constants/product-permissions';
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';

/**
 * 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;
}

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

async function resolveCampusTimezone(currentUser?: CurrentUser): Promise<string> {
  const campusId = getCampusId(currentUser);
  if (!campusId) {
    // Zone check-in is campus-staff-only; a user without a campus has no
    // "today" to compute.
    throw new ValidationError('zoneCheckinNoCampus');
  }
  const campus = await db.campuses.findByPk(campusId, {
    attributes: ['timezone'],
  });
  if (!campus) {
    throw new ValidationError('zoneCheckinNoCampus');
  }
  return campus.timezone;
}

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);
    assertCanZoneCheckIn(currentUser);
    if (!isZoneCheckinColor(data.zone)) {
      throw new ValidationError('zoneCheckinInvalidZone');
    }
    const timezone = await resolveCampusTimezone(currentUser);
    const date = localDateInTimezone(timezone);

    if (!isActingInOwnScope(currentUser)) {
      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 };
    }

    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);
    assertCanZoneCheckIn(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 };
  }
}

export default ZoneCheckinService;
