import {
  Op,
  cast,
  col,
  where,
  type WhereAttributeHash,
  type WhereOptions,
} from 'sequelize';
import db from '@/db/models';
import ValidationError from '@/shared/errors/validation';
import type { CurrentUser } from '@/db/api/types';

/** Text columns searched per table. */
const TABLE_COLUMNS: Record<string, string[]> = {
  users: ['firstName', 'lastName', 'phoneNumber', 'email'],
  organizations: ['name'],
  campuses: ['name', 'code', 'address', 'phone', 'email'],
  academic_years: ['name'],
  grades: ['name', 'code', 'description'],
  subjects: ['name', 'code', 'description'],
  staff: ['employee_number', 'job_title'],
  classes: ['name', 'section'],
  timetables: ['name'],
  timetable_periods: ['room'],
  attendance_sessions: ['notes'],
  attendance_records: ['remarks'],
  assessments: ['name', 'instructions'],
  assessment_results: ['remarks'],
  messages: ['subject', 'body'],
  message_recipients: ['recipient_label', 'destination'],
};

/** Numeric columns searched per table (cast to text before matching). */
const COLUMNS_INT: Record<string, string[]> = {
  grades: ['sort_order'],
  classes: ['capacity'],
  attendance_records: ['minutes_late'],
  assessments: ['max_score'],
  assessment_results: ['score'],
};

async function checkPermissions(
  permission: string,
  currentUser?: CurrentUser,
): Promise<boolean> {
  if (!currentUser) {
    throw new ValidationError('auth.unauthorized');
  }

  const customPermissions = currentUser.custom_permissions ?? [];
  if (customPermissions.some((cp) => cp.name === permission)) {
    return true;
  }

  if (!currentUser.app_role) {
    throw new ValidationError('auth.forbidden');
  }

  const getPermissions = currentUser.app_role.getPermissions;
  const permissions = getPermissions
    ? await getPermissions.call(currentUser.app_role)
    : [];

  return permissions.some((p) => p.name === permission);
}

class SearchService {
  static async search(
    searchQuery: string | undefined,
    currentUser: CurrentUser | undefined,
    organizationId: string | null | undefined,
    globalAccess: boolean,
  ): Promise<Record<string, unknown>[]> {
    if (!searchQuery) {
      throw new ValidationError('iam.errors.searchQueryRequired');
    }

    const query = searchQuery.toLowerCase();
    let allFoundRecords: Record<string, unknown>[] = [];

    for (const tableName of Object.keys(TABLE_COLUMNS)) {
      const attributesToSearch = TABLE_COLUMNS[tableName];
      const attributesIntToSearch = COLUMNS_INT[tableName] ?? [];

      const stringConditions: WhereOptions[] = attributesToSearch.map(
        (attribute) => ({ [attribute]: { [Op.iLike]: `%${searchQuery}%` } }),
      );
      const intConditions: WhereOptions[] = attributesIntToSearch.map(
        (attribute) =>
          where(cast(col(`${tableName}.${attribute}`), 'varchar'), {
            [Op.iLike]: `%${searchQuery}%`,
          }),
      );

      const whereCondition: WhereAttributeHash = {
        [Op.or]: [...stringConditions, ...intConditions],
      };

      if (!globalAccess && tableName !== 'organizations' && organizationId) {
        whereCondition.organizationId = organizationId;
      }

      const hasPermission = await checkPermissions(
        `READ_${tableName.toUpperCase()}`,
        currentUser,
      );
      if (!hasPermission) {
        continue;
      }

      const model = db.sequelize.models[tableName];
      const foundRecords = await model.findAll({
        where: whereCondition,
        attributes: [...attributesToSearch, 'id', ...attributesIntToSearch],
      });

      const modifiedRecords = foundRecords.map((record) => {
        const matchAttribute: string[] = [];

        for (const attribute of attributesToSearch) {
          const value: unknown = record.get(attribute);
          if (typeof value === 'string' && value.toLowerCase().includes(query)) {
            matchAttribute.push(attribute);
          }
        }

        for (const attribute of attributesIntToSearch) {
          const castedValue = String(record.get(attribute));
          if (castedValue.toLowerCase().includes(query)) {
            matchAttribute.push(attribute);
          }
        }

        const plain: Record<string, unknown> = record.get();
        return { ...plain, matchAttribute, tableName };
      });

      allFoundRecords = allFoundRecords.concat(modifiedRecords);
    }

    return allFoundRecords;
  }
}

export default SearchService;
