import { Op } from 'sequelize';
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 {
  getOwnTenant,
  getOrganizationId,
  getSchoolId,
  hasFeaturePermission,
  tenantExactWhere,
  tenantStamp,
} from '@/services/shared/access';
import {
  PER_TENANT_CONTENT_TYPES,
  SCHOOL_SCOPED_CONTENT_TYPES,
  ORG_SCOPED_CONTENT_TYPES,
  TENANT_SCOPED_CONTENT_TYPES,
} from '@/shared/constants/content-catalog';
import { FEATURE_PERMISSIONS } from '@/shared/constants/product-permissions';
import type { ContentCatalog } from '@/db/models/content_catalog';
import type { CurrentUser } from '@/db/api/types';

/** Scope where for tenant-scoped content types; `{}` for shared/global types. */
function tenantWhereFor(
  contentType: string,
  currentUser?: CurrentUser,
): Record<string, unknown> {
  if (PER_TENANT_CONTENT_TYPES.has(contentType)) {
    return tenantExactWhere(getOwnTenant(currentUser));
  }
  if (SCHOOL_SCOPED_CONTENT_TYPES.has(contentType)) {
    // Everyone in the school reads their school's row (the user's school is
    // resolved from their own school/campus chain).
    return { schoolId: getSchoolId(currentUser) ?? null };
  }
  if (ORG_SCOPED_CONTENT_TYPES.has(contentType)) {
    return { organizationId: getOrganizationId(currentUser) ?? null };
  }
  return {};
}

/** Owning ids to stamp on a tenant-scoped content row; all null for shared. */
function tenantStampFor(contentType: string, currentUser?: CurrentUser) {
  if (PER_TENANT_CONTENT_TYPES.has(contentType)) {
    return tenantStamp(getOwnTenant(currentUser));
  }
  if (SCHOOL_SCOPED_CONTENT_TYPES.has(contentType)) {
    return {
      organizationId: getOrganizationId(currentUser),
      schoolId: getSchoolId(currentUser),
      campusId: null,
      classId: null,
    };
  }
  if (ORG_SCOPED_CONTENT_TYPES.has(contentType)) {
    return {
      organizationId: getOrganizationId(currentUser),
      schoolId: null,
      campusId: null,
      classId: null,
    };
  }
  return { organizationId: null, schoolId: null, campusId: null, classId: null };
}

interface ContentCatalogInput {
  content_type?: unknown;
  payload?: unknown;
  active?: boolean;
  importHash?: string | null;
}

function toContentCatalogDto(record: ContentCatalog) {
  const plain = record.get({ plain: true });

  return {
    id: plain.id,
    content_type: plain.content_type,
    payload: plain.payload,
    updatedAt: plain.updatedAt,
  };
}

function assertCanManageContentCatalog(currentUser?: CurrentUser): void {
  if (
    hasFeaturePermission(
      currentUser,
      FEATURE_PERMISSIONS.MANAGE_CONTENT_CATALOG,
    )
  ) {
    return;
  }

  throw new ForbiddenError();
}

/**
 * Edit authorization per content type. Tenant scoping constrains the row; the
 * right to edit content catalog data is a single effective permission.
 */
function assertCanManageType(currentUser?: CurrentUser): void {
  if (
    !hasFeaturePermission(
      currentUser,
      FEATURE_PERMISSIONS.MANAGE_CONTENT_CATALOG,
    )
  ) {
    throw new ForbiddenError();
  }
}

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

  return contentType.trim();
}

function assertValidPayload(payload: unknown): unknown {
  if (payload === undefined) {
    throw new ValidationError();
  }

  return payload;
}

class ContentCatalogService {
  static async list(
    filter: { limit?: number | string; page?: number | string } = {},
    currentUser?: CurrentUser,
  ) {
    assertCanManageContentCatalog(currentUser);

    const { limit, offset } = resolvePagination(filter.limit, filter.page);

    // Per-tenant content types are managed via their dedicated editor (by type +
    // tenant), so they are excluded from the shared management list.
    const result = await db.content_catalog.findAndCountAll({
      where: { content_type: { [Op.notIn]: [...TENANT_SCOPED_CONTENT_TYPES] } },
      order: [['content_type', 'asc']],
      limit,
      offset,
    });

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

  static async findByType(contentType: unknown, currentUser?: CurrentUser) {
    const normalizedContentType = assertValidContentType(contentType);

    // Tenant-scoped content is never served on the unauthenticated public path.
    if (TENANT_SCOPED_CONTENT_TYPES.has(normalizedContentType) && !currentUser?.id) {
      throw new ValidationError('contentCatalogNotFound');
    }

    const record = await db.content_catalog.findOne({
      where: {
        content_type: normalizedContentType,
        active: true,
        ...tenantWhereFor(normalizedContentType, currentUser),
      },
    });

    if (!record) {
      throw new ValidationError('contentCatalogNotFound');
    }

    return toContentCatalogDto(record);
  }

  static async findManagedByType(contentType: unknown, currentUser?: CurrentUser) {
    assertCanManageContentCatalog(currentUser);

    return this.findByType(contentType, currentUser);
  }

  static async create(data: ContentCatalogInput, currentUser?: CurrentUser) {
    const contentType = assertValidContentType(data?.content_type);
    assertCanManageType(currentUser);

    const payload = assertValidPayload(data?.payload);
    const tenantWhere = tenantWhereFor(contentType, currentUser);
    const stamp = tenantStampFor(contentType, currentUser);

    const existingRecord = await db.content_catalog.findOne({
      where: { content_type: contentType, ...tenantWhere },
      paranoid: false,
    });

    if (existingRecord && !existingRecord.deletedAt) {
      throw new ValidationError();
    }

    return withTransaction(async (transaction) => {
      let record: ContentCatalog;
      if (existingRecord) {
        await existingRecord.restore({ transaction });
        record = await existingRecord.update(
          {
            payload,
            active: data.active !== false,
            importHash: data.importHash || null,
          },
          { transaction },
        );
      } else {
        record = await db.content_catalog.create(
          {
            content_type: contentType,
            payload,
            active: data.active !== false,
            importHash: data.importHash || null,
            ...stamp,
          },
          { transaction },
        );
      }

      return toContentCatalogDto(record);
    });
  }

  static async update(
    contentType: unknown,
    data: ContentCatalogInput,
    currentUser?: CurrentUser,
  ) {
    const normalizedContentType = assertValidContentType(contentType);
    assertCanManageType(currentUser);

    const payload = assertValidPayload(data?.payload);

    return withTransaction(async (transaction) => {
      const record = await db.content_catalog.findOne({
        where: {
          content_type: normalizedContentType,
          ...tenantWhereFor(normalizedContentType, currentUser),
        },
        transaction,
      });

      if (!record) {
        throw new ValidationError('contentCatalogNotFound');
      }

      await record.update(
        {
          payload,
          active: data.active !== false,
        },
        { transaction },
      );

      return toContentCatalogDto(record);
    });
  }

  static async delete(contentType: unknown, currentUser?: CurrentUser) {
    const normalizedContentType = assertValidContentType(contentType);
    assertCanManageType(currentUser);

    return withTransaction(async (transaction) => {
      const record = await db.content_catalog.findOne({
        where: {
          content_type: normalizedContentType,
          ...tenantWhereFor(normalizedContentType, currentUser),
        },
        transaction,
      });

      if (!record) {
        throw new ValidationError('contentCatalogNotFound');
      }

      await record.update({ active: false }, { transaction });
      await record.destroy({ transaction });
      return true;
    });
  }
}

export default ContentCatalogService;
