import db from '@/db/models';
import { withTransaction } from '@/db/with-transaction';
import { resolvePagination } from '@/shared/constants/pagination';
import ForbiddenError from '@/shared/errors/forbidden';
import ValidationError from '@/shared/errors/validation';
import {
  getOrganizationIdOrGlobal,
  hasRoleAccess,
} from '@/services/shared/access';
import { FRAME_EDITOR_ROLE_NAMES } from '@/shared/constants/frame';
import type { FrameEntries } from '@/db/models/frame_entries';
import type { CurrentUser } from '@/db/api/types';

interface FrameEntryInput {
  week_of: string;
  posted_date: string;
  formal: string;
  recognition: string;
  application: string;
  management: string;
  emotional: string;
  author: string;
  campusId?: string | null;
}

const REQUIRED_FIELDS = [
  'week_of',
  'posted_date',
  'formal',
  'recognition',
  'application',
  'management',
  'emotional',
  'author',
] as const;

function getCampusId(
  currentUser: CurrentUser | undefined,
  data: FrameEntryInput,
): string | null {
  if (data.campusId) {
    return data.campusId;
  }

  const staff = currentUser?.staff_user;
  if (Array.isArray(staff) && staff[0]?.campusId) {
    return staff[0].campusId;
  }

  return null;
}

function assertCanEdit(currentUser?: CurrentUser): void {
  if (hasRoleAccess(currentUser, FRAME_EDITOR_ROLE_NAMES)) {
    return;
  }

  throw new ForbiddenError();
}

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

  const hasMissingField = REQUIRED_FIELDS.some((field) => {
    const value = data[field];
    return typeof value !== 'string' || value.trim().length === 0;
  });

  if (hasMissingField) {
    throw new ValidationError();
  }
}

function toDto(entry: FrameEntries) {
  const plain = entry.get({ plain: true });

  return {
    id: plain.id,
    week_of: plain.week_of,
    posted_date: plain.posted_date,
    formal: plain.formal,
    recognition: plain.recognition,
    application: plain.application,
    management: plain.management,
    emotional: plain.emotional,
    author: plain.author,
    organizationId: plain.organizationId,
    campusId: plain.campusId,
    createdAt: plain.createdAt,
    updatedAt: plain.updatedAt,
  };
}

class FrameEntriesService {
  static async list(
    filter: { limit?: number | string; page?: number | string } = {},
    currentUser?: CurrentUser,
  ) {
    const organizationId = getOrganizationIdOrGlobal(currentUser);
    const { limit, offset } = resolvePagination(filter.limit, filter.page);

    const where = organizationId ? { organizationId } : {};

    const result = await db.frame_entries.findAndCountAll({
      where,
      order: [['createdAt', 'desc']],
      limit,
      offset,
    });

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

  static async create(data: FrameEntryInput, currentUser?: CurrentUser) {
    assertCanEdit(currentUser);
    assertValidFrameEntry(data);

    const organizationId = getOrganizationIdOrGlobal(currentUser);

    return withTransaction(async (transaction) => {
      const entry = await db.frame_entries.create(
        {
          week_of: data.week_of.trim(),
          posted_date: data.posted_date.trim(),
          formal: data.formal.trim(),
          recognition: data.recognition.trim(),
          application: data.application.trim(),
          management: data.management.trim(),
          emotional: data.emotional.trim(),
          author: data.author.trim(),
          organizationId,
          campusId: getCampusId(currentUser, data),
          createdById: currentUser?.id ?? null,
          updatedById: currentUser?.id ?? null,
        },
        { transaction },
      );

      return toDto(entry);
    });
  }

  static async update(
    id: string,
    data: FrameEntryInput,
    currentUser?: CurrentUser,
  ) {
    assertCanEdit(currentUser);
    assertValidFrameEntry(data);

    const organizationId = getOrganizationIdOrGlobal(currentUser);
    const where = organizationId ? { id, organizationId } : { id };

    return withTransaction(async (transaction) => {
      const entry = await db.frame_entries.findOne({
        where,
        transaction,
      });

      if (!entry) {
        throw new ValidationError();
      }

      await entry.update(
        {
          week_of: data.week_of.trim(),
          posted_date: data.posted_date.trim(),
          formal: data.formal.trim(),
          recognition: data.recognition.trim(),
          application: data.application.trim(),
          management: data.management.trim(),
          emotional: data.emotional.trim(),
          author: data.author.trim(),
          campusId: getCampusId(currentUser, data),
          updatedById: currentUser?.id ?? null,
        },
        { transaction },
      );

      return toDto(entry);
    });
  }

  static async destroy(id: string, currentUser?: CurrentUser) {
    assertCanEdit(currentUser);

    const organizationId = getOrganizationIdOrGlobal(currentUser);
    const where = organizationId ? { id, organizationId } : { id };

    return withTransaction(async (transaction) => {
      const entry = await db.frame_entries.findOne({ where, transaction });

      if (!entry) {
        throw new ValidationError();
      }

      await entry.destroy({ transaction });
    });
  }
}

export default FrameEntriesService;
