import { Op } from 'sequelize';
import db from '@/db/models';
import { withTransaction } from '@/db/with-transaction';
import ForbiddenError from '@/shared/errors/forbidden';
import ValidationError from '@/shared/errors/validation';
import {
  getOrganizationIdOrGlobal,
  getCampusId,
  getSchoolId,
  getClassId,
  assertAuthenticatedTenantUser,
  campusDimensionScope,
  hasFeaturePermission,
  isActingInOwnScope,
  getDisplayName,
  getRoleScope,
  requireUserId,
} from '@/services/shared/access';
import { FEATURE_PERMISSIONS } from '@/shared/constants/product-permissions';
import { ROLE_NAMES, ROLE_SCOPES } from '@/shared/constants/roles';
import { toWeekStartIso } from '@/shared/constants/week';
import type { PersonalityQuizResults } from '@/db/models/personality_quiz_results';
import type { Users } from '@/db/models/users';
import type { CurrentUser } from '@/db/api/types';

interface PersonalityInput {
  quiz_kind?: string;
  quiz_id: string;
  quiz_title: string;
  personality_type?: string | null;
  quiz_answers: unknown;
  score?: number | null;
  total_questions: number;
  result_label?: string | null;
  result_payload?: unknown;
}

interface PersonalityFilter {
  campusId?: string;
  quiz_kind?: string;
}

interface PersonalityHistoryFilter {
  quiz_kind?: string;
  limit?: string | number;
}

const PERSONALITY_QUIZ_KIND = 'personality_type';
const EI_SELF_ASSESSMENT_KIND = 'ei_self_assessment';
const DEFAULT_HISTORY_LIMIT = 25;
const MAX_HISTORY_LIMIT = 100;
const ALLOWED_QUIZ_KINDS = Object.freeze([
  PERSONALITY_QUIZ_KIND,
  EI_SELF_ASSESSMENT_KIND,
]);
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;
}

function normalizeQuizKind(value: unknown): string {
  if (typeof value !== 'string' || value.trim().length === 0) {
    return PERSONALITY_QUIZ_KIND;
  }

  const normalized = value.trim();
  if (!ALLOWED_QUIZ_KINDS.includes(normalized)) {
    throw new ValidationError();
  }

  return normalized;
}

function parseHistoryLimit(value: unknown): number {
  if (value === undefined || value === null || value === '') {
    return DEFAULT_HISTORY_LIMIT;
  }

  const limit = Number(value);
  if (!Number.isInteger(limit) || limit < 1) {
    throw new ValidationError();
  }

  return Math.min(limit, MAX_HISTORY_LIMIT);
}

function getProductRole(currentUser?: CurrentUser): string {
  return currentUser?.app_role?.name ?? ROLE_NAMES.TEACHER;
}

function currentWeekStartIso(): string {
  const today = new Date().toISOString().slice(0, 10);
  return toWeekStartIso(today) ?? today;
}

function weekWhereForQuizKind(quizKind: string): { week_of?: string } {
  return quizKind === EI_SELF_ASSESSMENT_KIND ? { week_of: currentWeekStartIso() } : {};
}

function assertValidResult(data: PersonalityInput): void {
  if (!data || typeof data !== 'object' || Array.isArray(data)) {
    throw new ValidationError();
  }

  const quizKind = normalizeQuizKind(data.quiz_kind);
  if (
    typeof data.quiz_id !== 'string' ||
    data.quiz_id.trim().length === 0 ||
    typeof data.quiz_title !== 'string' ||
    data.quiz_title.trim().length === 0 ||
    !data.quiz_answers ||
    typeof data.quiz_answers !== 'object' ||
    Array.isArray(data.quiz_answers) ||
    !Number.isInteger(data.total_questions)
  ) {
    throw new ValidationError();
  }

  const answerValues = Object.values(data.quiz_answers);
  if (answerValues.length === 0) {
    throw new ValidationError();
  }

  if (quizKind === PERSONALITY_QUIZ_KIND) {
    if (
      typeof data.personality_type !== 'string' ||
      data.personality_type.trim().length === 0 ||
      !answerValues.every(
        (value) => typeof value === 'string' && value.trim().length > 0,
      )
    ) {
      throw new ValidationError();
    }
    return;
  }

  if (
    data.score !== null &&
    data.score !== undefined &&
    !Number.isInteger(data.score)
  ) {
    throw new ValidationError();
  }
}

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 latestResultsByUserAndKind(
  results: readonly PersonalityQuizResults[],
): ReadonlyMap<string, PersonalityQuizResults> {
  const byUserAndKind = new Map<string, PersonalityQuizResults>();
  for (const result of results) {
    const userId = result.userId;
    if (!userId) {
      continue;
    }
    const key = `${userId}:${result.quiz_kind}`;
    if (byUserAndKind.has(key)) {
      continue;
    }
    byUserAndKind.set(key, result);
  }
  return byUserAndKind;
}

function toDto(record: PersonalityQuizResults | null) {
  if (!record) {
    return null;
  }

  const plain = record.get({ plain: true });

  return {
    id: plain.id,
    quiz_kind: plain.quiz_kind,
    quiz_id: plain.quiz_id,
    quiz_title: plain.quiz_title,
    week_of: plain.week_of,
    personality_type: plain.personality_type,
    quiz_answers: plain.quiz_answers,
    score: plain.score,
    total_questions: plain.total_questions,
    result_label: plain.result_label,
    result_payload: plain.result_payload,
    user_name: plain.user_name,
    user_role: plain.user_role,
    completed_at: plain.completed_at,
    organizationId: plain.organizationId,
    campusId: plain.campusId,
    userId: plain.userId,
    createdById: plain.createdById,
    updatedById: plain.updatedById,
    createdAt: plain.createdAt,
    updatedAt: plain.updatedAt,
  };
}

class PersonalityQuizResultsService {
  static async getCurrentUserResult(
    filter: PersonalityFilter,
    currentUser?: CurrentUser,
  ) {
    assertAuthenticatedTenantUser(currentUser);

    const organizationId = getOrganizationIdOrGlobal(currentUser);
    const orgFilter = organizationId ? { organizationId } : {};
    const quizKind = normalizeQuizKind(filter.quiz_kind);

    const record = await db.personality_quiz_results.findOne({
      where: {
        ...orgFilter,
        userId: requireUserId(currentUser),
        quiz_kind: quizKind,
        ...weekWhereForQuizKind(quizKind),
      },
      order: [['completed_at', 'desc']],
    });

    return toDto(record);
  }

  static async getCurrentUserHistory(
    filter: PersonalityHistoryFilter,
    currentUser?: CurrentUser,
  ) {
    assertAuthenticatedTenantUser(currentUser);

    const organizationId = getOrganizationIdOrGlobal(currentUser);
    const orgFilter = organizationId ? { organizationId } : {};
    const quizKindFilter = filter.quiz_kind
      ? { quiz_kind: normalizeQuizKind(filter.quiz_kind) }
      : {};

    const rows = await db.personality_quiz_results.findAll({
      where: {
        ...orgFilter,
        ...quizKindFilter,
        userId: requireUserId(currentUser),
      },
      order: [['completed_at', 'desc']],
      limit: parseHistoryLimit(filter.limit),
    });

    return {
      rows: rows.map(toDto),
      count: rows.length,
    };
  }

  static async upsertCurrentUserResult(
    data: PersonalityInput,
    currentUser?: CurrentUser,
  ) {
    assertAuthenticatedTenantUser(currentUser);
    assertValidResult(data);

    if (!isActingInOwnScope(currentUser)) {
      return null;
    }

    const organizationId = getOrganizationIdOrGlobal(currentUser);
    const quizKind = normalizeQuizKind(data.quiz_kind);
    const weekOf = quizKind === EI_SELF_ASSESSMENT_KIND ? currentWeekStartIso() : null;
    return withTransaction(async (transaction) => {
      const payload = {
        quiz_kind: quizKind,
        quiz_id: data.quiz_id.trim(),
        quiz_title: data.quiz_title.trim(),
        week_of: weekOf,
        personality_type: data.personality_type
          ? data.personality_type.trim().toUpperCase()
          : null,
        quiz_answers: data.quiz_answers,
        score: data.score ?? null,
        total_questions: data.total_questions,
        result_label: data.result_label?.trim() || data.personality_type?.trim().toUpperCase() || null,
        result_payload: data.result_payload ?? null,
        user_name: getDisplayName(currentUser),
        user_role: getProductRole(currentUser),
        completed_at: new Date(),
        organizationId,
        campusId: getCampusId(currentUser),
        userId: currentUser?.id ?? null,
        createdById: currentUser?.id ?? null,
        updatedById: currentUser?.id ?? null,
      };

      const saved = await db.personality_quiz_results.create(payload, {
        transaction,
      });

      return toDto(saved);
    });
  }

  static async distribution(
    filter: PersonalityFilter,
    currentUser?: CurrentUser,
  ) {
    assertAuthenticatedTenantUser(currentUser);

    if (
      !hasFeaturePermission(
        currentUser,
        FEATURE_PERMISSIONS.READ_PERSONALITY_REPORTS,
      )
    ) {
      throw new ForbiddenError();
    }

    const organizationId = getOrganizationIdOrGlobal(currentUser);
    const orgFilter = organizationId ? { organizationId } : {};

    const rows = await db.personality_quiz_results.findAll({
      where: {
        ...orgFilter,
        quiz_kind: PERSONALITY_QUIZ_KIND,
        // School/campus isolation; an explicit campusId filter is intersected
        // (Op.and) with the scope so a school role cannot read another school.
        [Op.and]: [
          campusDimensionScope(currentUser),
          ...(filter.campusId ? [{ campusId: filter.campusId }] : []),
        ],
      },
      order: [['completed_at', 'desc']],
    });
    const latestByUser = new Map<string, PersonalityQuizResults>();
    for (const row of rows) {
      if (!row.userId || latestByUser.has(row.userId)) {
        continue;
      }
      latestByUser.set(row.userId, row);
    }
    const counts = new Map<string, number>();
    for (const row of latestByUser.values()) {
      if (!row.personality_type) {
        continue;
      }
      counts.set(row.personality_type, (counts.get(row.personality_type) ?? 0) + 1);
    }
    const sortedRows = [...counts.entries()]
      .sort(([, countA], [, countB]) => countB - countA)
      .map(([type, count]) => ({ type, count }));

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

  static async completion(filter: PersonalityFilter, currentUser?: CurrentUser) {
    assertAuthenticatedTenantUser(currentUser);
    if (
      !hasFeaturePermission(
        currentUser,
        FEATURE_PERMISSIONS.READ_PERSONALITY_REPORTS,
      )
    ) {
      throw new ForbiddenError();
    }

    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 userIds = staffUsers.map((user) => user.id);
    const quizKinds = filter.quiz_kind
      ? [normalizeQuizKind(filter.quiz_kind)]
      : [EI_SELF_ASSESSMENT_KIND, PERSONALITY_QUIZ_KIND];
    const organizationId = getOrganizationIdOrGlobal(currentUser);
    const orgFilter = organizationId ? { organizationId } : {};
    const resultWhere = filter.quiz_kind
      ? {
          ...orgFilter,
          userId: { [Op.in]: userIds },
          quiz_kind: quizKinds[0],
          ...weekWhereForQuizKind(quizKinds[0]),
        }
      : {
          ...orgFilter,
          userId: { [Op.in]: userIds },
          [Op.or]: [
            { quiz_kind: PERSONALITY_QUIZ_KIND },
            {
              quiz_kind: EI_SELF_ASSESSMENT_KIND,
              week_of: currentWeekStartIso(),
            },
          ],
        };
    const results = userIds.length > 0
      ? await db.personality_quiz_results.findAll({
          where: resultWhere,
          order: [['completed_at', 'desc']],
        })
      : [];
    const resultByUserAndKind = latestResultsByUserAndKind(results);
    const rows = staffUsers.map((user) => {
      const selfAssessment = resultByUserAndKind.get(`${user.id}:${EI_SELF_ASSESSMENT_KIND}`) ?? null;
      const personality = resultByUserAndKind.get(`${user.id}:${PERSONALITY_QUIZ_KIND}`) ?? null;
      const tenant = tenantOf(user);
      const completedKinds: string[] = [];
      if (selfAssessment) {
        completedKinds.push(EI_SELF_ASSESSMENT_KIND);
      }
      if (personality) {
        completedKinds.push(PERSONALITY_QUIZ_KIND);
      }

      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,
        status: completedKinds.length >= quizKinds.length ? 'complete' : 'pending',
        completedKinds,
        selfAssessment: selfAssessment ? toDto(selfAssessment) : null,
        personality: personality ? toDto(personality) : null,
      };
    });
    const selfAssessmentCompletedCount = rows.filter((row) => row.selfAssessment).length;
    const personalityCompletedCount = rows.filter((row) => row.personality).length;
    const completedCount = rows.filter((row) => row.status === 'complete').length;

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

export default PersonalityQuizResultsService;
