import db from '@/db/models';
import { withTransaction } from '@/db/with-transaction';
import ValidationError from '@/shared/errors/validation';
import ForbiddenError from '@/shared/errors/forbidden';
import {
  assertAuthenticatedTenantUser,
  getCampusId,
  getOwnTenant,
  getOrganizationIdOrGlobal,
  hasFeaturePermission,
  isActingInOwnScope,
  requireUserId,
  tenantExactWhere,
} from '@/services/shared/access';
import { tenantWhere } from '@/db/api/shared/repository';
import { resolvePagination } from '@/shared/constants/pagination';
import { ROLE_NAMES } from '@/shared/constants/roles';
import { FEATURE_PERMISSIONS } from '@/shared/constants/product-permissions';
import type { PolicyAcknowledgments } from '@/db/models/policy_acknowledgments';
import type { CurrentUser } from '@/db/api/types';

interface AcknowledgeInput {
  policyDocumentId?: unknown;
}

interface AcknowledgmentsFilter {
  policyDocumentId?: string;
  limit?: number | string;
  page?: number | string;
}

const ACKNOWLEDGMENT_REPORT_STAFF_ROLES = Object.freeze([
  ROLE_NAMES.DIRECTOR,
  ROLE_NAMES.OFFICE_MANAGER,
  ROLE_NAMES.TEACHER,
  ROLE_NAMES.SUPPORT_STAFF,
]);

interface TenantReference {
  readonly name?: string | null;
  readonly logo?: string | null;
}

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

function requireDocumentId(value: unknown): string {
  if (typeof value !== 'string' || value.trim().length === 0) {
    throw new ValidationError();
  }
  return value.trim();
}

function toDto(record: PolicyAcknowledgments) {
  const plain = record.get({ plain: true });
  return {
    id: plain.id,
    policyDocumentId: plain.policyDocumentId,
    version: plain.version,
    userId: plain.userId,
    acknowledgedAt: plain.acknowledgedAt,
    organizationId: plain.organizationId,
    campusId: plain.campusId,
    createdAt: plain.createdAt,
    updatedAt: plain.updatedAt,
  };
}

function assertCanReadReport(currentUser?: CurrentUser): void {
  assertAuthenticatedTenantUser(currentUser);
  if (currentUser?.app_role?.globalAccess === true) {
    if (currentUser?.activeScope) {
      return;
    }
    throw new ForbiddenError();
  }
  if (
    hasFeaturePermission(
      currentUser,
      FEATURE_PERMISSIONS.READ_POLICY_ACKNOWLEDGMENT_REPORTS,
    )
  ) {
    return;
  }
  throw new ForbiddenError();
}

function staffScopeWhere(currentUser?: CurrentUser) {
  const tenant = getOwnTenant(currentUser);
  const base = tenant.organizationId ? { organizationId: tenant.organizationId } : {};

  if (tenant.level === 'school') {
    return { ...base, schoolId: tenant.schoolId };
  }
  if (tenant.level === 'campus') {
    return { ...base, campusId: tenant.campusId };
  }
  if (tenant.level === 'class') {
    return { ...base, classId: tenant.classId };
  }
  return base;
}

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

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

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

class PolicyAcknowledgmentsService {
  /** A campus staff member's own acknowledgments (optionally for one document). */
  static async list(
    filter: AcknowledgmentsFilter,
    currentUser?: CurrentUser,
  ) {
    assertAuthenticatedTenantUser(currentUser);
    assertCanAcknowledge(currentUser);
    const { limit, offset } = resolvePagination(filter.limit, filter.page);

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

    const result = await db.policy_acknowledgments.findAndCountAll({
      where: {
        ...orgFilter,
        userId: requireUserId(currentUser),
        ...(filter.policyDocumentId
          ? { policyDocumentId: filter.policyDocumentId }
          : {}),
      },
      order: [['acknowledgedAt', 'desc']],
      limit,
      offset,
    });

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

  /**
   * Record the current user's acknowledgment of a document's **current version**.
   * Idempotent per (user, document, version): re-acknowledging the same version
   * returns the existing row; a bumped document version requires a new one.
   */
  static async acknowledge(data: AcknowledgeInput, currentUser?: CurrentUser) {
    assertAuthenticatedTenantUser(currentUser);
    assertCanAcknowledge(currentUser);
    const policyDocumentId = requireDocumentId(data.policyDocumentId);

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

    return withTransaction(async (transaction) => {
      // Resolve the document within the caller's tenant; its current version is
      // what gets acknowledged (the client cannot spoof an older version).
      const document = await db.policy_documents.findOne({
        where: { id: policyDocumentId, ...tenantWhere(currentUser) },
        transaction,
      });
      if (!document) {
        throw new ValidationError('policyDocumentNotFound');
      }

      const version = document.version ?? 1;
      const userId = requireUserId(currentUser);

      const existing = await db.policy_acknowledgments.findOne({
        where: { userId, policyDocumentId, version },
        transaction,
      });
      if (existing) {
        return toDto(existing);
      }

      const created = await db.policy_acknowledgments.create(
        {
          policyDocumentId,
          version,
          userId,
          acknowledgedAt: new Date(),
          organizationId: document.organizationId ?? null,
          campusId: getCampusId(currentUser) ?? document.campusId ?? null,
          createdById: userId,
          updatedById: userId,
        },
        { transaction },
      );

      return toDto(created);
    });
  }

  static async report(currentUser?: CurrentUser) {
    assertCanReadReport(currentUser);
    const tenant = getOwnTenant(currentUser);

    const documents = await db.policy_documents.findAll({
      where: {
        active: true,
        ...tenantExactWhere(tenant),
      },
      order: [
        ['category', 'asc'],
        ['title', 'asc'],
      ],
    });

    const staffUsers = await db.users.findAll({
      where: {
        disabled: false,
        ...staffScopeWhere(currentUser),
      },
      include: [
        {
          model: db.roles,
          as: 'app_role',
          required: true,
          where: { name: ACKNOWLEDGMENT_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 documentIds = documents.map((document) => document.id);
    const userIds = staffUsers.map((user) => user.id);

    const acknowledgments =
      documentIds.length > 0 && userIds.length > 0
        ? await db.policy_acknowledgments.findAll({
            where: {
              policyDocumentId: documentIds,
              userId: userIds,
            },
          })
        : [];

    const acknowledgedAtByKey = new Map<string, Date>();
    for (const acknowledgment of acknowledgments) {
      acknowledgedAtByKey.set(
        `${acknowledgment.userId}:${acknowledgment.policyDocumentId}:${acknowledgment.version}`,
        acknowledgment.acknowledgedAt,
      );
    }

    const documentRows = documents.map((document) => {
      let acknowledgedCount = 0;
      for (const user of staffUsers) {
        if (acknowledgedAtByKey.has(`${user.id}:${document.id}:${document.version}`)) {
          acknowledgedCount += 1;
        }
      }
      const missingCount = staffUsers.length - acknowledgedCount;
      return {
        id: document.id,
        title: document.title,
        category: document.category,
        version: document.version,
        totalStaff: staffUsers.length,
        acknowledgedCount,
        missingCount,
        completionRate: staffUsers.length > 0
          ? Math.round((acknowledgedCount / staffUsers.length) * 100)
          : 0,
      };
    });

    const staffRows = staffUsers.map((user) => {
      const tenant = tenantOf(user);
      const documentStatuses = documents.map((document) => {
        const acknowledgedAt = acknowledgedAtByKey.get(
          `${user.id}:${document.id}:${document.version}`,
        ) ?? null;
        return {
          policyDocumentId: document.id,
          title: document.title,
          category: document.category,
          version: document.version,
          acknowledgedAt,
          missing: acknowledgedAt === null,
        };
      });
      const acknowledgedCount = documentStatuses.filter((status) => !status.missing).length;
      const missingCount = documentStatuses.length - acknowledgedCount;
      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,
        campusId: user.campusId ?? null,
        schoolId: user.schoolId ?? null,
        classId: user.classId ?? null,
        acknowledgedCount,
        missingCount,
        completionRate: documentStatuses.length > 0
          ? Math.round((acknowledgedCount / documentStatuses.length) * 100)
          : 0,
        documents: documentStatuses,
      };
    });

    const totalRequired = documents.length * staffUsers.length;
    const acknowledgedCount = staffRows.reduce(
      (sum, staff) => sum + staff.acknowledgedCount,
      0,
    );
    const missingCount = Math.max(totalRequired - acknowledgedCount, 0);

    return {
      summary: {
        scope: tenant.level,
        totalDocuments: documents.length,
        totalStaff: staffUsers.length,
        totalRequired,
        acknowledgedCount,
        missingCount,
        completionRate: totalRequired > 0
          ? Math.round((acknowledgedCount / totalRequired) * 100)
          : 0,
      },
      documents: documentRows,
      staff: staffRows,
    };
  }
}

export default PolicyAcknowledgmentsService;
