import { Op } from 'sequelize';
import type { Transaction } 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 {
  getRoleScope,
  getOwnTenant,
  getOrganizationId,
  getCampusId,
  getSchoolId,
  hasFeaturePermission,
  tenantExactWhere,
  tenantStamp,
} from '@/services/shared/access';
import {
  CLASSROOM_SUPPORT_CONTENT_TYPE,
  ESA_CONTENT_TYPE,
  EI_ASSESSMENT_CONTENT_TYPE,
  PERSONALITY_QUIZ_CONTENT_TYPE,
  SIGN_LANGUAGE_ITEMS_CONTENT_TYPE,
  SAFETY_QUIZ_CONTENT_TYPE,
  PER_TENANT_CONTENT_TYPES,
  CAMPUS_SCOPED_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 { ROLE_SCOPES } from '@/shared/constants/roles';
import { POLICY_DOCUMENT_CATEGORIES } from '@/shared/constants/policy-documents';
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 (CAMPUS_SCOPED_CONTENT_TYPES.has(contentType)) {
    const tenant = getOwnTenant(currentUser);
    const campusId = getCampusId(currentUser) ?? tenant.campusId ?? null;
    return {
      organizationId: tenant.organizationId,
      schoolId: null,
      campusId,
      classId: null,
    };
  }
  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 (CAMPUS_SCOPED_CONTENT_TYPES.has(contentType)) {
    const tenant = getOwnTenant(currentUser);
    const campusId = getCampusId(currentUser) ?? tenant.campusId;
    return {
      organizationId: tenant.organizationId,
      schoolId: null,
      campusId,
      classId: null,
    };
  }
  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;
  changeSummary?: unknown;
}

const VERSIONED_CONTENT_TYPES = new Set([
  SAFETY_QUIZ_CONTENT_TYPE,
  EI_ASSESSMENT_CONTENT_TYPE,
  PERSONALITY_QUIZ_CONTENT_TYPE,
]);

const ESA_POLICY_TAG = 'ESA Funding';
const ESA_POLICY_BODY = 'Staff must review the current ESA Funding Information page and acknowledge understanding of the campus guidance.';

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(contentType: string, currentUser?: CurrentUser): void {
  if (
    !hasFeaturePermission(
      currentUser,
      FEATURE_PERMISSIONS.MANAGE_CONTENT_CATALOG,
    )
    && !(
      contentType === ESA_CONTENT_TYPE
      && hasFeaturePermission(
        currentUser,
        FEATURE_PERMISSIONS.MANAGE_ESA_FUNDING_CONTENT,
      )
    )
  ) {
    throw new ForbiddenError();
  }

  if (
    contentType === ESA_CONTENT_TYPE
    && getRoleScope(currentUser) !== ROLE_SCOPES.CAMPUS
  ) {
    throw new ForbiddenError();
  }

  if (
    (
      contentType === CLASSROOM_SUPPORT_CONTENT_TYPE
      || contentType === SAFETY_QUIZ_CONTENT_TYPE
      || contentType === EI_ASSESSMENT_CONTENT_TYPE
      || contentType === PERSONALITY_QUIZ_CONTENT_TYPE
      || contentType === SIGN_LANGUAGE_ITEMS_CONTENT_TYPE
    )
    && getRoleScope(currentUser) !== ROLE_SCOPES.ORGANIZATION
  ) {
    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;
}

function optionalChangeSummary(value: unknown): string | null {
  if (typeof value !== 'string') {
    return null;
  }

  const trimmed = value.trim();
  return trimmed.length > 0 ? trimmed.slice(0, 120) : null;
}

function esaPolicyBody(changeSummary: string | null): string {
  return changeSummary
    ? `${ESA_POLICY_BODY}\n\nUpdated section: ${changeSummary}`
    : ESA_POLICY_BODY;
}

async function touchEsaPolicyDocument(
  contentType: string,
  currentUser: CurrentUser | undefined,
  changeSummary: string | null,
  transaction?: Transaction,
): Promise<void> {
  if (contentType !== ESA_CONTENT_TYPE) {
    return;
  }

  const where = {
    category: POLICY_DOCUMENT_CATEGORIES.HANDBOOK_POLICY,
    tag: ESA_POLICY_TAG,
    active: true,
    ...tenantWhereFor(ESA_CONTENT_TYPE, currentUser),
  };
  const existing = await db.policy_documents.findOne({ where, transaction });

  if (existing) {
    await existing.update(
      {
        body: esaPolicyBody(changeSummary),
        version: (existing.version ?? 1) + 1,
      },
      { transaction },
    );
    return;
  }

  await db.policy_documents.create(
    {
      title: 'ESA Funding Information',
      body: esaPolicyBody(changeSummary),
      category: POLICY_DOCUMENT_CATEGORIES.HANDBOOK_POLICY,
      tag: ESA_POLICY_TAG,
      author: null,
      steps: null,
      autism_considerations: null,
      version: 1,
      active: true,
      importHash: null,
      ...tenantStampFor(ESA_CONTENT_TYPE, currentUser),
    },
    { transaction },
  );
}

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) {
    const normalizedContentType = assertValidContentType(contentType);
    assertCanManageType(normalizedContentType, currentUser);

    return this.findByType(normalizedContentType, currentUser);
  }

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

    const payload = assertValidPayload(data?.payload);
    const changeSummary = optionalChangeSummary(data?.changeSummary);
    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 },
        );
      }

      await touchEsaPolicyDocument(contentType, currentUser, changeSummary, transaction);

      return toContentCatalogDto(record);
    });
  }

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

    const payload = assertValidPayload(data?.payload);
    const changeSummary = optionalChangeSummary(data?.changeSummary);

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

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

      if (VERSIONED_CONTENT_TYPES.has(normalizedContentType)) {
        await record.update({ active: false }, { transaction });
        const version = await db.content_catalog.create(
          {
            content_type: normalizedContentType,
            payload,
            active: data.active !== false,
            importHash: data.importHash || null,
            ...tenantStampFor(normalizedContentType, currentUser),
          },
          { transaction },
        );

        return toContentCatalogDto(version);
      }

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

      await touchEsaPolicyDocument(normalizedContentType, currentUser, changeSummary, transaction);

      return toContentCatalogDto(record);
    });
  }

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

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

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

      await record.update({ active: false }, { transaction });
      await record.destroy({ transaction });
      await touchEsaPolicyDocument(normalizedContentType, currentUser, 'Content removed', transaction);
      return true;
    });
  }
}

export default ContentCatalogService;
