import db from '@/db/models';
import { Op } from 'sequelize';
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 { optionalIsoDate } from '@/services/shared/validate';
import {
  getOwnTenant,
  tenantExactWhere,
  tenantStamp,
  hasFeaturePermission,
} from '@/services/shared/access';
import { FEATURE_PERMISSIONS } from '@/shared/constants/product-permissions';
import { toWeekStartIso } from '@/shared/constants/week';
import type { FrameEntries } from '@/db/models/frame_entries';
import type { CurrentUser } from '@/db/api/types';

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

interface FrameEntryFilter {
  readonly limit?: number | string;
  readonly page?: number | string;
  readonly startDate?: unknown;
  readonly endDate?: unknown;
}

/** Normalizes the input week to its Sunday-start ISO date, or throws. */
function requireWeekStart(weekOf: string): string {
  const weekStart = toWeekStartIso(weekOf);
  if (!weekStart) {
    throw new ValidationError();
  }
  return weekStart;
}

/** Optional free-text label (e.g. "Spring Break week"); null when blank. */
function toWeekLabel(value: unknown): string | null {
  return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
}

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

function assertCanEdit(currentUser?: CurrentUser): void {
  if (
    hasFeaturePermission(
      currentUser,
      FEATURE_PERMISSIONS.MANAGE_FRAME,
    )
  ) {
    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 weekRangeFilter(filter: FrameEntryFilter) {
  const startDate = optionalIsoDate(filter.startDate);
  const endDate = optionalIsoDate(filter.endDate);

  if (!startDate && !endDate) {
    return {};
  }

  return {
    week_of: {
      ...(startDate ? { [Op.gte]: startDate } : {}),
      ...(endDate ? { [Op.lte]: endDate } : {}),
    },
  };
}

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

  return {
    id: plain.id,
    week_of: plain.week_of,
    week_label: plain.week_label,
    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,
    schoolId: plain.schoolId,
    classId: plain.classId,
    createdAt: plain.createdAt,
    updatedAt: plain.updatedAt,
  };
}

class FrameEntriesService {
  static async list(
    filter: FrameEntryFilter = {},
    currentUser?: CurrentUser,
  ) {
    const { limit, offset } = resolvePagination(filter.limit, filter.page);

    // Per-tenant content: a user sees the FRAME entries dedicated to their own
    // tenant level (org/school/campus/class), not an aggregate of children.
    const where = {
      ...tenantExactWhere(getOwnTenant(currentUser)),
      ...weekRangeFilter(filter),
    };

    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);

    // New entries are stamped to the author's own tenant level (drill-down
    // authoring of a child tenant arrives with the scope-context UI).
    const stamp = tenantStamp(getOwnTenant(currentUser));

    return withTransaction(async (transaction) => {
      const entry = await db.frame_entries.create(
        {
          week_of: requireWeekStart(data.week_of),
          week_label: toWeekLabel(data.week_label),
          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: stamp.organizationId,
          schoolId: stamp.schoolId,
          campusId: stamp.campusId,
          classId: stamp.classId,
          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);

    // Only entries owned by the user's own tenant level are editable here.
    const where = { id, ...tenantExactWhere(getOwnTenant(currentUser)) };

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

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

      await entry.update(
        {
          week_of: requireWeekStart(data.week_of),
          week_label: toWeekLabel(data.week_label),
          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(),
          updatedById: currentUser?.id ?? null,
        },
        { transaction },
      );

      return toDto(entry);
    });
  }

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

    const where = { id, ...tenantExactWhere(getOwnTenant(currentUser)) };

    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;
