import { v4 as uuid } from 'uuid';
import { QueryTypes, type CreationAttributes, type QueryInterface } from 'sequelize';
import {
  ROLE_DEFINITIONS,
  ROLE_NAMES,
  type RoleName,
} from '@/shared/constants/roles';
import { SEED_ALL_USERS } from '@/shared/constants/seed-fixtures';
import {
  MODULE_READ_ALL_STAFF,
  MODULE_READ_INSTRUCTIONAL,
  MODULE_READ_PARENT_COMM,
  MODULE_READ_EXTERNAL,
  MODULE_READ_DIRECTOR,
  MODULE_ACTIONS,
  MODULE_PERMISSIONS,
} from '@/shared/constants/product-permissions';
import type { Roles } from '@/db/models/roles';
import type { Permissions } from '@/db/models/permissions';

/**
 * Seeds the first-class roles (Workstream 3 §3.1), the permission catalog,
 * the role→permission preset matrix (§3.2), and assigns roles to the seeded
 * users. Pre-launch, this fully replaces the old generated role set.
 *
 * Enforcement model:
 * - `super_admin` keeps the global-scope bypass for platform recovery/admin.
 * - `system_admin` keeps global scope reach but is permission-driven like the
 *   tenant roles, so the role receives an explicit full permission matrix.
 * - `owner` / `superintendent` / `principal` / `director` get full-scope
 *   baseline permissions, constrained at runtime by scope/tenant rules.
 * - `office_manager` / `teacher` / `support_staff` get read-only entity access.
 * - `student` / `guardian` / `guest` get no entity CRUD permissions; their
 *   access comes from explicit product-feature permissions.
 */

export const PERMISSION_ENTITIES = [
  'users', 'roles', 'permissions', 'organizations', 'schools', 'campuses',
  'academic_years', 'grades', 'subjects',
  'classes', 'class_enrollments', 'class_subjects', 'guardian_students', 'timetables',
  'timetable_periods', 'attendance_sessions', 'attendance_records',
  'assessments', 'assessment_results', 'messages',
  'message_recipients', 'policy_documents',
];

export const CRUD_VERBS = ['CREATE', 'READ', 'UPDATE', 'DELETE'] as const;
export const EXTRA_PERMISSIONS = ['READ_API_DOCS', 'CREATE_SEARCH'] as const;
export const FULL_ACCESS_EXCLUDED_FOR_ALL = ['READ_PARENT_COMM'] as const;
export const FULL_ACCESS_EXCLUDED_FOR_NON_CAMPUS_STAFF = [
  'ACK_POLICY',
] as const;

/** Roles granted every permission (full CRUD within their tenant/scope). */
export const FULL_ACCESS_ROLES: readonly RoleName[] = [
  ROLE_NAMES.OWNER,
  ROLE_NAMES.SUPERINTENDENT,
  // School head: full access, constrained to the school by tenant scoping.
  ROLE_NAMES.PRINCIPAL,
  ROLE_NAMES.DIRECTOR,
];

/** Roles granted read-only access to tenant resources. */
export const READ_ONLY_ROLES: readonly RoleName[] = [
  // Registrar: the Principal's read-only/audit assistant (school-wide visibility).
  ROLE_NAMES.REGISTRAR,
  ROLE_NAMES.OFFICE_MANAGER,
  ROLE_NAMES.TEACHER,
  ROLE_NAMES.SUPPORT_STAFF,
];

/** External (non-staff) roles. */
export const EXTERNAL_ROLES: readonly RoleName[] = [
  ROLE_NAMES.STUDENT,
  ROLE_NAMES.GUARDIAN,
];

// Product module/page permissions (§3.2) come from the shared single source
// `shared/constants/product-permissions.ts`, which the feature routes also use
// to enforce them — so granted names and checked names never drift.

/**
 * Per-role product-feature grants for the non-global, non-full-access roles.
 * `office_manager` excludes instructional tools but includes parent comms and may fill
 * attendance; `teacher` includes instructional tools + parent comms; both
 * `teacher` and `support_staff` take quizzes / leave read receipts;
 * `student` gets external pages; `guardian` gets external pages plus parent comms.
 */
export const MODULE_PERMISSIONS_BY_ROLE: Partial<Record<RoleName, readonly string[]>> = {
  // Registrar: read every product surface across the school for audit, and
  // complete required staff safety training, but no operational write actions.
  [ROLE_NAMES.REGISTRAR]: [
    ...MODULE_READ_ALL_STAFF,
    ...MODULE_READ_INSTRUCTIONAL,
    ...MODULE_READ_EXTERNAL,
    ...MODULE_READ_DIRECTOR,
    'READ_STAFF_ATTENDANCE_REPORTS',
    'READ_SAFETY_QUIZ_REPORTS',
    'READ_PERSONALITY_REPORTS',
    'READ_ZONE_CHECKIN_REPORTS',
    'READ_POLICY_ACKNOWLEDGMENT_REPORTS',
    'TAKE_QUIZ',
    'ZONE_CHECKIN',
    'READ_AUDIO_FILES',
  ],
  [ROLE_NAMES.OFFICE_MANAGER]: [
    ...MODULE_READ_ALL_STAFF,
    ...MODULE_READ_PARENT_COMM,
    ...MODULE_READ_EXTERNAL,
    ...MODULE_ACTIONS,
    'MANAGE_ESA_FUNDING_CONTENT',
    'READ_AUDIO_FILES', 'MANAGE_AUDIO_FILES',
  ],
  [ROLE_NAMES.TEACHER]: [
    ...MODULE_READ_ALL_STAFF,
    ...MODULE_READ_INSTRUCTIONAL,
    ...MODULE_READ_PARENT_COMM,
    ...MODULE_READ_EXTERNAL,
    // Teacher fills their class's attendance (rolls up to campus/school/org).
    'FILL_ATTENDANCE',
    'TAKE_QUIZ', 'ACK_READ_RECEIPT', 'ACK_POLICY', 'ZONE_CHECKIN',
    'READ_AUDIO_FILES', 'MANAGE_AUDIO_FILES',
  ],
  [ROLE_NAMES.SUPPORT_STAFF]: [
    ...MODULE_READ_ALL_STAFF,
    ...MODULE_READ_INSTRUCTIONAL,
    ...MODULE_READ_EXTERNAL,
    'TAKE_QUIZ', 'ACK_READ_RECEIPT', 'ACK_POLICY', 'ZONE_CHECKIN',
    'READ_AUDIO_FILES',
  ],
  [ROLE_NAMES.STUDENT]: [...MODULE_READ_EXTERNAL],
  // Guardians communicate with school staff via Parent Communication / Messages.
  [ROLE_NAMES.GUARDIAN]: [...MODULE_READ_EXTERNAL, ...MODULE_READ_PARENT_COMM],
};

export function buildEntityPermissionNames(): readonly string[] {
  const names: string[] = [];
  for (const entity of PERMISSION_ENTITIES) {
    for (const verb of CRUD_VERBS) {
      names.push(`${verb}_${entity.toUpperCase()}`);
    }
  }
  return names;
}

function uniquePermissionNames(names: readonly string[]): readonly string[] {
  return [...new Set(names)];
}

export function buildSeededPermissionNamesForRole(role: RoleName): readonly string[] {
  const entityPermissionNames = buildEntityPermissionNames();
  const allPermissionNames = [
    ...entityPermissionNames,
    ...EXTRA_PERMISSIONS,
    ...MODULE_PERMISSIONS,
  ];

  if (role === ROLE_NAMES.SYSTEM_ADMIN) {
    return uniquePermissionNames(allPermissionNames);
  }

  if (FULL_ACCESS_ROLES.includes(role)) {
    const excluded = new Set<string>(FULL_ACCESS_EXCLUDED_FOR_ALL);
    if (role !== ROLE_NAMES.DIRECTOR) {
      for (const name of FULL_ACCESS_EXCLUDED_FOR_NON_CAMPUS_STAFF) {
        excluded.add(name);
      }
    }
    return uniquePermissionNames(allPermissionNames.filter((name) => !excluded.has(name)));
  }

  if (READ_ONLY_ROLES.includes(role)) {
    return uniquePermissionNames([
      ...entityPermissionNames.filter((name) => name.startsWith('READ_')),
      ...(MODULE_PERMISSIONS_BY_ROLE[role] ?? []),
    ]);
  }

  return uniquePermissionNames(MODULE_PERMISSIONS_BY_ROLE[role] ?? []);
}

export default {
  async up(queryInterface: QueryInterface) {
    const createdAt = new Date();
    const updatedAt = new Date();

    const roleId = new Map<string, string>();
    const permId = new Map<string, string>();

    // 1. Roles.
    const existingRoles = await queryInterface.sequelize.query<{
      id: string;
      name: string;
    }>(
      `SELECT id, name FROM roles WHERE name IN (:names)`,
      {
        replacements: { names: ROLE_DEFINITIONS.map((role) => role.name) },
        type: QueryTypes.SELECT,
      },
    );
    for (const role of existingRoles) {
      roleId.set(role.name, role.id);
    }

    const roleRows: CreationAttributes<Roles>[] = ROLE_DEFINITIONS.map(
      (role) => {
        const id = roleId.get(role.name) ?? uuid();
        roleId.set(role.name, id);
        return {
          id,
          name: role.name,
          scope: role.scope,
          globalAccess: role.globalAccess,
          createdAt,
          updatedAt,
        };
      },
    ).filter((role) => !existingRoles.some((existing) => existing.id === role.id));
    if (roleRows.length > 0) {
      await queryInterface.bulkInsert('roles', roleRows);
    }

    // 2. Permissions (entity CRUD + extras + product module/page perms).
    const entityPermissionNames = [...buildEntityPermissionNames()];

    const allPermissionNames = uniquePermissionNames([
      ...entityPermissionNames,
      ...EXTRA_PERMISSIONS,
      ...MODULE_PERMISSIONS,
    ]);
    const existingPermissions = await queryInterface.sequelize.query<{
      id: string;
      name: string;
    }>(
      `SELECT id, name FROM permissions WHERE name IN (:names)`,
      {
        replacements: { names: allPermissionNames },
        type: QueryTypes.SELECT,
      },
    );
    for (const permission of existingPermissions) {
      permId.set(permission.name, permission.id);
    }

    const permissionRows: CreationAttributes<Permissions>[] =
      allPermissionNames.map((name) => {
        const id = permId.get(name) ?? uuid();
        permId.set(name, id);
        return { id, name, createdAt, updatedAt };
      }).filter(
        (permission) =>
          !existingPermissions.some((existing) => existing.id === permission.id),
      );
    if (permissionRows.length > 0) {
      await queryInterface.bulkInsert('permissions', permissionRows);
    }

    // 3. Role → permission matrix.
    const links: Array<{
      createdAt: Date;
      updatedAt: Date;
      roles_permissionsId: string;
      permissionId: string;
    }> = [];

    const grant = (role: RoleName, names: readonly string[]): void => {
      const rid = roleId.get(role);
      if (!rid) return;
      for (const name of names) {
        const pid = permId.get(name);
        if (pid) {
          links.push({
            createdAt,
            updatedAt,
            roles_permissionsId: rid,
            permissionId: pid,
          });
        }
      }
    };

    for (const role of [
      ROLE_NAMES.SYSTEM_ADMIN,
      ...FULL_ACCESS_ROLES,
      ...READ_ONLY_ROLES,
      ...EXTERNAL_ROLES,
    ]) {
      grant(role, buildSeededPermissionNamesForRole(role));
    }
    // Workstream 11: the office_manager also manages policy documents; director
    // receives the same writes through the seeded full-scope permission matrix,
    // while teacher/support stay read-only.
    grant(ROLE_NAMES.OFFICE_MANAGER, [
      'CREATE_POLICY_DOCUMENTS',
      'UPDATE_POLICY_DOCUMENTS',
      'DELETE_POLICY_DOCUMENTS',
    ]);

    const roleIds = [...roleId.values()];
    const permissionIds = [...permId.values()];
    const existingLinks = await queryInterface.sequelize.query<{
      roles_permissionsId: string;
      permissionId: string;
    }>(
      `SELECT "roles_permissionsId", "permissionId"
         FROM "rolesPermissionsPermissions"
        WHERE "roles_permissionsId" IN (:roleIds)
          AND "permissionId" IN (:permissionIds)`,
      {
        replacements: { roleIds, permissionIds },
        type: QueryTypes.SELECT,
      },
    );
    const existingLinkKeys = new Set(
      existingLinks.map(
        (link) => `${link.roles_permissionsId}:${link.permissionId}`,
      ),
    );
    const missingLinkKeys = new Set<string>();
    const missingLinks = links.filter((link) => {
      const key = `${link.roles_permissionsId}:${link.permissionId}`;
      if (existingLinkKeys.has(key) || missingLinkKeys.has(key)) {
        return false;
      }
      missingLinkKeys.add(key);
      return true;
    });

    if (missingLinks.length > 0) {
      await queryInterface.bulkInsert('rolesPermissionsPermissions', missingLinks);
    }

    // 4. Assign roles to the seeded fixture users (by id — robust to the
    //    configured super-admin email).
    for (const fixture of SEED_ALL_USERS) {
      const rid = roleId.get(fixture.role);
      if (!rid) continue;
      await queryInterface.sequelize.query(
        `UPDATE "users" SET "app_roleId"=:rid WHERE "id"=:id`,
        { replacements: { rid, id: fixture.id } },
      );
    }
  },

  async down(queryInterface: QueryInterface) {
    await queryInterface.bulkDelete('rolesPermissionsPermissions', {});
    await queryInterface.bulkDelete('permissions', {});
    await queryInterface.bulkDelete('roles', {});
  },
};
