import { nullableString } from '@/services/shared/validate';
import { isRecord } from '@/shared/object';
import db from '@/db/models';
import { withTransaction } from '@/db/with-transaction';
import ForbiddenError from '@/shared/errors/forbidden';
import ValidationError from '@/shared/errors/validation';
import {
  assertAuthenticatedTenantUser,
  campusScope,
  getCampusId,
  getOrganizationId,
  getOrganizationIdOrGlobal,
  hasGlobalAccess,
  hasRoleAccess,
  requireOrganizationId,
  requireUserId,
} from '@/services/shared/access';
import {
  COMMUNICATION_AUDIENCES,
  COMMUNICATION_CHANNELS,
  COMMUNICATION_EVENT_TYPE_VALUES,
  type CommunicationEventType,
  COMMUNICATION_MANAGER_ROLE_NAMES,
  COMMUNICATION_RECIPIENT_TYPES,
  COMMUNICATION_STATUSES,
  COMMUNICATION_TENANT_WIDE_ROLE_NAMES,
  DEFAULT_PARENT_MESSAGE_CATEGORY,
  PARENT_MESSAGE_CATEGORY_VALUES,
  type ParentMessageCategory,
} from '@/shared/constants/communications';
import { ROLE_NAMES, type RoleName } from '@/shared/constants/roles';
import { resolvePagination } from '@/shared/constants/pagination';
import type { Messages } from '@/db/models/messages';
import type { CommunicationEvents } from '@/db/models/communication_events';
import type { CurrentUser } from '@/db/api/types';
import type {
  ParentMessageFilter,
  ParentMessageInput,
  EventFilter,
  EventInput,
} from '@/services/communications.types';

const DEFAULT_EVENT_ROLES: RoleName[] = [
  ROLE_NAMES.TEACHER,
  ROLE_NAMES.SUPPORT_STAFF,
  ROLE_NAMES.OFFICE_MANAGER,
  ROLE_NAMES.DIRECTOR,
];
const ROLE_NAME_LIST: readonly RoleName[] = Object.values(ROLE_NAMES);

function requireEventType(value: unknown): CommunicationEventType {
  const type = requiredString(value);
  const match = COMMUNICATION_EVENT_TYPE_VALUES.find((item) => item === type);
  if (!match) {
    throw new ValidationError();
  }
  return match;
}

function assertCanManageCommunications(currentUser?: CurrentUser): void {
  assertAuthenticatedTenantUser(currentUser);

  if (hasRoleAccess(currentUser, COMMUNICATION_MANAGER_ROLE_NAMES)) {
    return;
  }

  throw new ForbiddenError();
}

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

  return value.trim();
}

function validateRoles(roles: unknown): RoleName[] {
  if (!Array.isArray(roles) || roles.length === 0) {
    return [...DEFAULT_EVENT_ROLES];
  }

  return roles.map((role: unknown) => {
    const normalized = typeof role === 'string' ? role.trim() : role;
    const match = ROLE_NAME_LIST.find((item) => item === normalized);
    if (!match) {
      throw new ValidationError();
    }
    return match;
  });
}

function toParentMessageCategory(subject: unknown): ParentMessageCategory {
  const match = PARENT_MESSAGE_CATEGORY_VALUES.find((item) => item === subject);
  return match ?? DEFAULT_PARENT_MESSAGE_CATEGORY;
}

function toIsoString(value: Date | string | null): string {
  return new Date(value ?? Date.now()).toISOString();
}

function toParentMessageDto(message: Messages | null) {
  if (!message) {
    return null;
  }

  const plain = message.get({ plain: true });
  const recipients = message.get('message_recipients_message');
  const firstRecipient = Array.isArray(recipients) ? recipients[0] : null;
  const recipientLabel =
    isRecord(firstRecipient) && typeof firstRecipient.recipient_label === 'string'
      ? firstRecipient.recipient_label
      : '';

  return {
    id: plain.id,
    text: plain.body ?? '',
    to: recipientLabel,
    date: toIsoString(plain.sent_at ?? plain.createdAt),
    category: toParentMessageCategory(plain.subject),
    sentAt: toIsoString(plain.sent_at ?? plain.createdAt),
    organizationId: plain.organizationId,
    campusId: plain.campusId,
    createdById: plain.createdById,
    updatedById: plain.updatedById,
    createdAt: plain.createdAt,
    updatedAt: plain.updatedAt,
  };
}

function toCommunicationEventDto(event: CommunicationEvents) {
  const plain = event.get({ plain: true });

  return {
    id: plain.id,
    title: plain.title,
    date: plain.event_date,
    type: plain.event_type,
    roles: plain.roles,
    organizationId: plain.organizationId,
    campusId: plain.campusId,
    createdById: plain.createdById,
    updatedById: plain.updatedById,
    createdAt: plain.createdAt,
    updatedAt: plain.updatedAt,
  };
}

class CommunicationsService {
  static async listParentMessages(
    filter: ParentMessageFilter,
    currentUser?: CurrentUser,
  ) {
    assertAuthenticatedTenantUser(currentUser);
    const { limit, offset } = resolvePagination(filter.limit, filter.page);

    const organizationId = getOrganizationIdOrGlobal(currentUser);
    const orgFilter = organizationId ? { organizationId } : {};
    const createdByFilter = hasGlobalAccess(currentUser)
      ? {}
      : { createdById: currentUser?.id ?? null };

    const result = await db.messages.findAndCountAll({
      where: {
        ...orgFilter,
        ...createdByFilter,
        audience: COMMUNICATION_AUDIENCES.GUARDIANS,
        ...campusScope(currentUser, COMMUNICATION_TENANT_WIDE_ROLE_NAMES),
        ...(filter.category ? { subject: filter.category } : {}),
      },
      include: [
        {
          model: db.message_recipients,
          as: 'message_recipients_message',
          attributes: ['recipient_label'],
        },
      ],
      order: [
        ['sent_at', 'desc'],
        ['createdAt', 'desc'],
      ],
      limit,
      offset,
    });

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

  static async createParentMessage(
    data: ParentMessageInput,
    currentUser?: CurrentUser,
  ) {
    assertAuthenticatedTenantUser(currentUser);

    const recipientName = requiredString(data?.recipientName);
    const messageText = requiredString(data?.messageText);
    const category = toParentMessageCategory(nullableString(data?.category));

    const createdMessage = await withTransaction(async (transaction) => {
      const message = await db.messages.create(
        {
          subject: category,
          body: messageText,
          channel: COMMUNICATION_CHANNELS.IN_APP,
          audience: COMMUNICATION_AUDIENCES.GUARDIANS,
          sent_at: new Date(),
          status: COMMUNICATION_STATUSES.SENT,
          organizationId: getOrganizationId(currentUser),
          campusId: getCampusId(currentUser),
          sent_byId: currentUser?.id ?? null,
          createdById: currentUser?.id ?? null,
          updatedById: currentUser?.id ?? null,
        },
        { transaction },
      );

      await db.message_recipients.create(
        {
          recipient_type: COMMUNICATION_RECIPIENT_TYPES.GUARDIAN,
          recipient_label: recipientName,
          destination: null,
          delivery_status: COMMUNICATION_STATUSES.SENT,
          delivered_at: new Date(),
          read_at: null,
          organizationId: getOrganizationId(currentUser),
          messageId: message.id,
          createdById: currentUser?.id ?? null,
          updatedById: currentUser?.id ?? null,
        },
        { transaction },
      );

      return message;
    });

    const savedMessage = await db.messages.findByPk(createdMessage.id, {
      include: [
        {
          model: db.message_recipients,
          as: 'message_recipients_message',
          attributes: ['recipient_label'],
        },
      ],
    });

    return toParentMessageDto(savedMessage);
  }

  static async listEvents(filter: EventFilter, currentUser?: CurrentUser) {
    assertAuthenticatedTenantUser(currentUser);
    const { limit, offset } = resolvePagination(filter.limit, filter.page);

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

    const result = await db.communication_events.findAndCountAll({
      where: {
        ...orgFilter,
        ...campusScope(currentUser, COMMUNICATION_TENANT_WIDE_ROLE_NAMES),
        ...(filter.type ? { event_type: filter.type } : {}),
      },
      order: [
        ['event_date', 'asc'],
        ['createdAt', 'desc'],
      ],
      limit,
      offset,
    });

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

  static async createEvent(data: EventInput, currentUser?: CurrentUser) {
    assertCanManageCommunications(currentUser);

    const title = requiredString(data?.title);
    const date = requiredString(data?.date);
    const type = requireEventType(data?.type);

    const createdEvent = await db.communication_events.create({
      title,
      event_date: date,
      event_type: type,
      roles: validateRoles(data?.roles),
      organizationId: requireOrganizationId(currentUser),
      campusId: getCampusId(currentUser),
      createdById: requireUserId(currentUser),
      updatedById: currentUser?.id ?? null,
    });

    return toCommunicationEventDto(createdEvent);
  }
}

export default CommunicationsService;
