import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { ProgramSession, ProgramRegistrationOnlineSession, OnlineSession } from 'src/common/entities';
import { AppLoggerService } from 'src/common/services/logger.service';
import { handleKnownErrors } from 'src/common/utils/handle-error.util';
import { ERROR_CODES } from 'src/common/constants/error-string-constants';
import { OnlineTypeEnum } from 'src/common/enum/online-type.enum';
import { SessionProviderType } from 'src/common/enum/session-provider.enum';
import { ZoomWebinarStatus } from 'src/common/enum/zoom-webinar-status.enum';
import { SessionLinkModeEnum } from 'src/common/enum/session-link-mode.enum';
import {
  CreateSessionInput,
  UpdateSessionInput,
  CreateSharedSessionInput,
} from 'src/common/interfaces/online-session.interface';
import { ZoomWebinarRepository } from '../repositories/zoom-webinar.repository';
import { ZoomApiService } from '../services/zoom-api.service';
import { ZoomRole, ZoomRegistrantStatusAction } from '../enums/zoom-role.enum';
import { ZOOM_LOG } from 'src/common/constants/zoom.constants';
import { ZoomSessionBase } from './zoom-session.handler';
import {
  ZoomSessionHandler,
  ZoomContactInfo,
  ZoomParticipantResult,
} from '../interfaces/zoom-session.interface';
import { ZoomApiResponse, ZoomRecurrence } from '../interfaces/zoom-api.interface';

/** Meeting-specific Zoom session logic. */
@Injectable()
export class MeetingService extends ZoomSessionBase implements ZoomSessionHandler {
  constructor(
    repo: ZoomWebinarRepository,
    zoomApi: ZoomApiService,
    config: ConfigService,
    logger: AppLoggerService,
  ) {
    super(repo, zoomApi, config, logger);
  }

  async create(input: CreateSessionInput): Promise<ProgramSession> {
    const session = await this.loadSession(input.programSessionId);
    this.ensureNotProvisioned(session);
    const hostEmail = this.resolveHostEmail(input.hostEmail);
    // Default meetings to registration-based join unless explicitly opted out.
    const requireRegistration = input.requireRegistration !== false;

    const zoomResponse = await this.zoomApi.createMeeting(
      {
        title: this.buildSessionTitle(session, input.title),
        startAt: input.startAt,
        duration: input.duration,
        password: input.password,
        requireRegistration,
      },
      hostEmail,
    );
    const extId = zoomResponse?.id != null ? String(zoomResponse.id) : null;

    session.onlineType = OnlineTypeEnum.MEETING;
    session.onlineSession = new OnlineSession({
      programId: session.programId,
      programSessionId: session.id,
      type: OnlineTypeEnum.MEETING,
      provider: SessionProviderType.ZOOM,
      externalId: extId ?? '',
      joinUrl: zoomResponse?.join_url ?? '',
      password: zoomResponse?.password ?? input.password ?? '',
      registrationUrl: zoomResponse?.registration_url ?? null,
      requireRegistration,
      hostEmail,
      startUrl: zoomResponse?.start_url ?? null,
      status: input.status ?? ZoomWebinarStatus.DRAFT,
      // This path always provisions one distinct, non-recurring resource for
      // just this session — there is no "shared" concept here, unlike
      // createSharedLink's SHARED branch.
      linkMode: SessionLinkModeEnum.PER_SESSION,
      createdBy: input.actorUserId ?? null,
      updatedBy: input.actorUserId ?? null,
    });
    this.applyCommonFields(session, input);

    return this.persistWithRollback(
      session,
      extId,
      () => this.zoomApi.deleteMeeting(extId as string),
      ZOOM_LOG.MEETING_CREATED,
    );
  }

  // ---- Shared ("same link") recurring-resource hooks (see ZoomSessionBase) ----

  protected sharedOnlineType(): OnlineTypeEnum.WEBINAR | OnlineTypeEnum.MEETING {
    return OnlineTypeEnum.MEETING;
  }

  protected createSharedResource(
    input: CreateSharedSessionInput,
    anchor: ProgramSession,
    recurrence: ZoomRecurrence | undefined,
    hostEmail: string,
    duration: number,
  ): Promise<ZoomApiResponse> {
    return this.zoomApi.createMeeting(
      {
        title: this.buildSessionTitle(anchor, input.title),
        startAt: anchor.startsAt as Date,
        duration,
        password: input.password,
        // Meetings choose per-user registrant links vs one shared link; default on.
        requireRegistration: input.requireRegistration !== false,
        recurrence,
      },
      hostEmail,
    );
  }

  protected async updateOccurrence(
    externalId: string,
    occurrenceId: string,
    payload: { startAt: string; duration: number },
  ): Promise<void> {
    await this.zoomApi.updateMeetingOccurrence(externalId, occurrenceId, payload);
  }

  protected async deleteSharedResource(externalId: string): Promise<void> {
    await this.zoomApi.deleteMeeting(externalId);
  }

  async update(session: ProgramSession, input: UpdateSessionInput): Promise<ProgramSession> {
    const extId = session.onlineSession?.externalId;
    if (extId) {
      await this.zoomApi.updateMeeting(extId, {
        title: input.title,
        startAt: input.startAt,
        duration: input.duration,
        password: input.password,
      });
    }
    try {
      if (input.title !== undefined) session.name = input.title;
      if (input.startAt !== undefined) session.startsAt = new Date(input.startAt);
      if (input.duration !== undefined) session.duration = String(input.duration);
      if (input.password !== undefined && session.onlineSession) {
        session.onlineSession.password = input.password;
      }
      if (input.launchMode !== undefined && session.onlineSession) {
        session.onlineSession.launchMode = input.launchMode;
      }
      if (input.joinOpensMinutesBefore !== undefined && session.onlineSession) {
        session.onlineSession.joinOpensMinutesBefore = input.joinOpensMinutesBefore;
      }
      if (input.hostStartOpensMinutesBefore !== undefined && session.onlineSession) {
        session.onlineSession.hostStartOpensMinutesBefore = input.hostStartOpensMinutesBefore;
      }
      if (input.status !== undefined && session.onlineSession) {
        session.onlineSession.status = input.status;
      }
      if (input.registrationStartsAt !== undefined)
        session.registrationStartsAt = new Date(input.registrationStartsAt);
      if (input.registrationEndsAt !== undefined)
        session.registrationEndsAt = new Date(input.registrationEndsAt);
      if (input.actorUserId) session.updatedBy = input.actorUserId;

      const saved = await this.repo.save(session);
      this.logger.log(ZOOM_LOG.MEETING_UPDATED, { id: saved.id });
      return saved;
    } catch (error) {
      this.logger.error(ZOOM_LOG.MEETING_UPDATE_FAILED, error?.stack, { error, id: session.id });
      handleKnownErrors(ERROR_CODES.ZOOM_WEBINAR_SAVE_FAILED, error);
    }
  }

  async remove(session: ProgramSession, actorUserId?: number): Promise<void> {
    const extId = session.onlineSession?.externalId || null;
    if (extId) {
      // A shared recurring meeting backs several sessions — only delete it on Zoom
      // once the last session referencing it is removed, so siblings keep working.
      const isShared = session.onlineSession?.linkMode === SessionLinkModeEnum.SHARED;
      let isLastInGroup = true;
      if (isShared) {
        const siblings = await this.repo.findSiblingSessionsByExtId(extId);
        // Only the current session may still reference the resource. An empty
        // result is treated as "not last" so a lookup miss never deletes a
        // resource that siblings could still be using.
        isLastInGroup = siblings.length > 0 && siblings.every((s) => s.id === session.id);
      }
      if (isLastInGroup) await this.zoomApi.deleteMeeting(extId);
    }
    // Soft-delete the registrants' join-link extensions and the online_session
    // row (both set deleted_at), then detach the in-memory relation so the
    // session.save() below does not cascade-resurrect it. The unique indexes are
    // scoped to deleted_at IS NULL, so the slot is freed for re-provisioning.
    const onlineSessionId = session.onlineSession?.id ?? null;
    if (onlineSessionId) {
      await this.repo.softDeleteExtensionsByOnlineSessionId(onlineSessionId);
      await this.repo.deleteOnlineSession(onlineSessionId);
    }
    session.onlineSession = null;
    if (actorUserId) session.updatedBy = actorUserId;
    await this.repo.save(session);
    this.logger.log(ZOOM_LOG.MEETING_DELETED, { id: session.id });
  }

  /**
   * Meeting: no panelist concept (role ignored). Registration-based meetings get
   * a per-user registrant; shared-link meetings hand out the common join URL.
   */
  async addParticipant(
    session: ProgramSession,
    contact: ZoomContactInfo,
    role: ZoomRole,
    autoApprove = false,
  ): Promise<ZoomParticipantResult> {
    void role;
    const extId = session.onlineSession?.externalId || null;
    const requireRegistration = session.onlineSession?.requireRegistration !== false;
    if (!extId) return { joinUrl: null, zoomRegistrantId: null, isPanelist: false };

    if (requireRegistration) {
      const response = await this.zoomApi.addMeetingRegistrant(extId, {
        firstName: contact.firstName,
        lastName: contact.lastName,
        email: contact.email,
      });
      const zoomRegistrantId = response?.registrant_id ?? null;
      let joinUrl = response?.join_url ?? null;
      // Only meaningful when ZOOM_MANUAL_APPROVAL_REGISTRANTS is on (the meeting
      // was created with manual approval) — every caller of addParticipant in
      // this codebase passes autoApprove: true. While a registrant is pending,
      // Zoom's add-registrant response carries no usable join_url, so re-fetch
      // it after approving (same pattern as the cancelled-registrant restore
      // path below).
      if (autoApprove && zoomRegistrantId && this.manualApprovalEnabled()) {
        await this.zoomApi.updateMeetingRegistrantStatus(extId, ZoomRegistrantStatusAction.APPROVE, {
          id: zoomRegistrantId,
          email: contact.email,
        });
        const registrants = await this.zoomApi.fetchMeetingRegistrants(extId);
        const match = registrants.find((r) => r?.id != null && String(r.id) === zoomRegistrantId);
        joinUrl = match?.join_url ?? joinUrl;
      }
      return {
        joinUrl,
        zoomRegistrantId,
        isPanelist: false,
      };
    }
    // Shared-link meeting: no per-user registrant, hand out the common link.
    return {
      joinUrl: session.onlineSession?.joinUrl || null,
      zoomRegistrantId: null,
      isPanelist: false,
    };
  }

  /**
   * Cancels the registrant instead of deleting it by default, so
   * {@link approveParticipant} can restore the same join link later without
   * re-registering them — set `ZOOM_CANCEL_INSTEAD_OF_DELETE_REGISTRANT=false`
   * to fall back to a hard delete instead. No-op for a shared-link meeting,
   * where there is no per-user registrant to begin with.
   */
  async removeParticipant(
    session: ProgramSession,
    extension: ProgramRegistrationOnlineSession,
    contact: ZoomContactInfo | null,
  ): Promise<void> {
    const extId = session.onlineSession?.externalId || null;
    if (!extId || !extension.externalRegistrantId) return;

    if (!this.cancelInsteadOfDeleteRegistrant()) {
      await this.zoomApi.removeMeetingRegistrant(extId, extension.externalRegistrantId);
      return;
    }
    await this.zoomApi.updateMeetingRegistrantStatus(extId, ZoomRegistrantStatusAction.CANCEL, {
      id: extension.externalRegistrantId,
      email: contact?.email,
    });
  }

  /**
   * Re-approves a previously cancelled registrant on the same Zoom registrant
   * id, restoring their original join link. Falls back to a fresh
   * {@link addParticipant} when the extension holds no prior registrant id
   * (never registered, or a shared-link meeting with no per-user registrant),
   * or removal is running in hard-delete mode (no cancelled registrant left on
   * Zoom to re-approve).
   */
  async approveParticipant(
    session: ProgramSession,
    extension: ProgramRegistrationOnlineSession,
    contact: ZoomContactInfo,
  ): Promise<ZoomParticipantResult> {
    const extId = session.onlineSession?.externalId || null;
    if (!extId || !extension.externalRegistrantId || !this.cancelInsteadOfDeleteRegistrant()) {
      return this.addParticipant(session, contact, ZoomRole.ATTENDEE, true);
    }

    await this.zoomApi.updateMeetingRegistrantStatus(extId, ZoomRegistrantStatusAction.APPROVE, {
      id: extension.externalRegistrantId,
      email: contact.email,
    });
    const registrants = await this.zoomApi.fetchMeetingRegistrants(extId);
    const match = registrants.find((r) => r?.id != null && String(r.id) === extension.externalRegistrantId);
    return {
      joinUrl: match?.join_url ?? null,
      zoomRegistrantId: extension.externalRegistrantId,
      isPanelist: false,
    };
  }
}
