import DbApi from '@/db/api/schools';
import CampusesDBApi from '@/db/api/campuses';
import { createCrudService } from '@/services/shared/crud-service';
import { seedDefaultContentForTenant } from '@/services/content_catalog_seed';
import { withTransaction } from '@/db/with-transaction';
import { getRoleScope } from '@/services/shared/access';
import { ROLE_SCOPES } from '@/shared/constants/roles';
import ForbiddenError from '@/shared/errors/forbidden';
import ValidationError from '@/shared/errors/validation';
import type { CurrentUser } from '@/db/api/types';

interface FirstCampusInput {
  name?: string;
  code?: string;
  timezone?: string;
  logo?: string | null;
  address?: string | null;
  phone?: string | null;
  email?: string | null;
}

/** Lowercase slug for an auto-generated campus code from its name. */
function slugify(value: string): string {
  return (
    value
      .toLowerCase()
      .trim()
      .replace(/[^a-z0-9]+/g, '-')
      .replace(/^-+|-+$/g, '') || 'campus'
  );
}

/**
 * School provisioning is permission-gated at the route and scope-gated here:
 * only system/org scope can write school records. This keeps role names out of
 * the guard while preventing school/campus users from creating sibling schools.
 */
function assertCanWriteSchools(currentUser?: CurrentUser): void {
  const scope = getRoleScope(currentUser);
  if (scope === ROLE_SCOPES.SYSTEM || scope === ROLE_SCOPES.ORGANIZATION) return;
  throw new ForbiddenError();
}

const base = createCrudService(DbApi, { notFoundCode: 'schoolsNotFound' });

export default {
  ...base,
  async create(...args: Parameters<typeof base.create>) {
    assertCanWriteSchools(args[1]);
    return base.create(...args);
  },
  async update(...args: Parameters<typeof base.update>) {
    assertCanWriteSchools(args[2]);
    return base.update(...args);
  },
  async remove(...args: Parameters<typeof base.remove>) {
    assertCanWriteSchools(args[1]);
    return base.remove(...args);
  },
  async deleteByIds(...args: Parameters<typeof base.deleteByIds>) {
    assertCanWriteSchools(args[1]);
    return base.deleteByIds(...args);
  },

  /**
   * Atomically creates a school and its first campus (Organization → School →
   * Campus). The create form captures the first campus's name + timezone; both
   * rows are committed in one transaction (campus carries the new `schoolId`).
   * Restricted to system/org scope users with the route permission.
   */
  async createWithFirstCampus(
    data: Parameters<typeof base.create>[0],
    firstCampus: FirstCampusInput,
    currentUser?: CurrentUser,
  ) {
    assertCanWriteSchools(currentUser);

    const campusName = firstCampus?.name;
    const campusTimezone = firstCampus?.timezone;
    if (!campusName || !campusTimezone) {
      throw new ValidationError();
    }

    return withTransaction(async (transaction) => {
      const school = await DbApi.create(data, { currentUser, transaction });
      const campus = await CampusesDBApi.create(
        {
          name: campusName,
          code: firstCampus.code || slugify(campusName),
          timezone: campusTimezone,
          logo: firstCampus.logo ?? null,
          address: firstCampus.address ?? null,
          phone: firstCampus.phone ?? null,
          email: firstCampus.email ?? null,
          active: true,
          // Inherit the (possibly explicitly-targeted) org from the new school so
          // a global creator's first campus is not left org-less.
          organization: school.organizationId ?? undefined,
        },
        { currentUser, transaction },
      );
      await campus.setSchool(school.id, { transaction });

      // Preset the new school's and first campus's default content (idempotent).
      const organizationId = school.organizationId;
      if (organizationId) {
        await seedDefaultContentForTenant(
          { level: 'school', organizationId, schoolId: school.id },
          transaction,
        );
        await seedDefaultContentForTenant(
          { level: 'campus', organizationId, campusId: campus.id },
          transaction,
        );
      }

      return {
        school: school.get({ plain: true }),
        campus: campus.get({ plain: true }),
      };
    });
  },
};
