import { Clipboard } from '@angular/cdk/clipboard';
import { CommonModule } from '@angular/common';
import { Component, OnInit, Signal } from '@angular/core';
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { MatButtonModule, MatIconButton } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatRippleModule } from '@angular/material/core';
import { MatDividerModule } from '@angular/material/divider';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatMenuModule } from '@angular/material/menu';
import { MatTooltipModule } from '@angular/material/tooltip';
import { ActivatedRoute } from '@angular/router';
import { ShareMeetingLinkComponent } from '@lib/components/share-meeting-link/share-meeting-link.component';
import { ErrorReason } from '@lib/models';
import { CustomParticipantModel } from '@lib/models/custom-participant.model';
import {
	AppDataService,
	ApplicationFeatures,
	AuthService,
	FeatureConfigurationService,
	MeetingService,
	NavigationService,
	NotificationService,
	ParticipantService,
	RecordingService,
	RoomService,
	SessionStorageService,
	WebComponentManagerService
} from '@lib/services';
import {
	LeftEventReason,
	MeetRoom,
	MeetRoomStatus,
	ParticipantRole,
	WebComponentEvent,
	WebComponentOutboundEventMessage
} from '@lib/typings/ce';
import {
	MeetParticipantRoleUpdatedPayload,
	MeetRoomPreferencesUpdatedPayload,
	MeetSignalType
} from '@lib/typings/ce/event.model';
import {
	ApiDirectiveModule,
	ParticipantService as ComponentParticipantService,
	DataPacket_Kind,
	OpenViduComponentsUiModule,
	OpenViduService,
	ParticipantLeftEvent,
	ParticipantLeftReason,
	ParticipantModel,
	RecordingStartRequestedEvent,
	RecordingStopRequestedEvent,
	RemoteParticipant,
	Room,
	RoomEvent
} from 'openvidu-components-angular';
import { Subject, takeUntil } from 'rxjs';

@Component({
	selector: 'app-meeting',
	templateUrl: './meeting.component.html',
	styleUrls: ['./meeting.component.scss'],
	standalone: true,
	imports: [
		OpenViduComponentsUiModule,
		ApiDirectiveModule,
		CommonModule,
		MatFormFieldModule,
		MatInputModule,
		FormsModule,
		ReactiveFormsModule,
		MatCardModule,
		MatButtonModule,
		MatIconModule,
		MatIconButton,
		MatMenuModule,
		MatDividerModule,
		MatTooltipModule,
		MatRippleModule,
		ShareMeetingLinkComponent
	]
})
export class MeetingComponent implements OnInit {
	participantForm = new FormGroup({
		name: new FormControl('', [Validators.required])
	});

	hasRecordings = false;
	showRecordingCard = false;
	roomClosed = false;

	showBackButton = true;
	backButtonText = 'Back';

	room?: MeetRoom;
	roomId = '';
	roomSecret = '';
	participantName = '';
	participantToken = '';
	localParticipant?: CustomParticipantModel;
	remoteParticipants: CustomParticipantModel[] = [];

	showMeeting = false;
	features: Signal<ApplicationFeatures>;
	meetingEndedByMe = false;

	private destroy$ = new Subject<void>();

	constructor(
		protected route: ActivatedRoute,
		protected roomService: RoomService,
		protected meetingService: MeetingService,
		protected participantService: ParticipantService,
		protected recordingService: RecordingService,
		protected featureConfService: FeatureConfigurationService,
		protected authService: AuthService,
		protected appDataService: AppDataService,
		protected sessionStorageService: SessionStorageService,
		protected wcManagerService: WebComponentManagerService,
		protected openviduService: OpenViduService,
		protected componentParticipantService: ComponentParticipantService,
		protected navigationService: NavigationService,
		protected notificationService: NotificationService,
		protected clipboard: Clipboard
	) {
		this.features = this.featureConfService.features;
	}

	get roomName(): string {
		return this.room?.roomName || 'Room';
	}

	get hostname(): string {
		return window.location.origin.replace('http://', '').replace('https://', '');
	}

	async ngOnInit() {
		this.roomId = this.roomService.getRoomId();
		this.roomSecret = this.roomService.getRoomSecret();
		this.room = await this.roomService.getRoom(this.roomId);
		this.roomClosed = this.room.status === MeetRoomStatus.CLOSED;

		await this.setBackButtonText();
		await this.checkForRecordings();
		await this.initializeParticipantName();
	}

	ngOnDestroy() {
		this.destroy$.next();
		this.destroy$.complete();
	}

	/**
	 * Sets the back button text based on the application mode and user role
	 */
	private async setBackButtonText() {
		const isStandaloneMode = this.appDataService.isStandaloneMode();
		const redirection = this.navigationService.getLeaveRedirectURL();
		const isAdmin = await this.authService.isAdmin();

		if (isStandaloneMode && !redirection && !isAdmin) {
			// If in standalone mode, no redirection URL and not an admin, hide the back button
			this.showBackButton = false;
			return;
		}

		this.showBackButton = true;
		this.backButtonText = isStandaloneMode && !redirection && isAdmin ? 'Back to Rooms' : 'Back';
	}

	/**
	 * Checks if there are recordings in the room and updates the visibility of the recordings card.
	 *
	 * It is necessary to previously generate a recording token in order to list the recordings.
	 * If token generation fails or the user does not have sufficient permissions to list recordings,
	 * the error will be caught and the recordings card will be hidden (`showRecordingCard` will be set to `false`).
	 *
	 * If recordings exist, sets `showRecordingCard` to `true`; otherwise, to `false`.
	 */
	private async checkForRecordings() {
		try {
			const { canRetrieveRecordings } = await this.recordingService.generateRecordingToken(
				this.roomId,
				this.roomSecret
			);

			if (!canRetrieveRecordings) {
				this.showRecordingCard = false;
				return;
			}

			const { recordings } = await this.recordingService.listRecordings({
				maxItems: 1,
				roomId: this.roomId,
				fields: 'recordingId'
			});
			this.hasRecordings = recordings.length > 0;
			this.showRecordingCard = this.hasRecordings;
		} catch (error) {
			console.error('Error checking for recordings:', error);
			this.showRecordingCard = false;
		}
	}

	/**
	 * Initializes the participant name in the form control.
	 *
	 * Retrieves the participant name from the ParticipantTokenService first, and if not available,
	 * falls back to the authenticated username. Sets the retrieved name value in the
	 * participant form's 'name' control if a valid name is found.
	 *
	 * @returns A promise that resolves when the participant name has been initialized
	 */
	private async initializeParticipantName() {
		// Apply participant name from ParticipantTokenService if set, otherwise use authenticated username
		const currentParticipantName = this.participantService.getParticipantName();
		const username = await this.authService.getUsername();
		const participantName = currentParticipantName || username;

		if (participantName) {
			this.participantForm.get('name')?.setValue(participantName);
		}
	}

	async goToRecordings() {
		try {
			await this.navigationService.navigateTo(`room/${this.roomId}/recordings`, { secret: this.roomSecret });
		} catch (error) {
			console.error('Error navigating to recordings:', error);
		}
	}

	/**
	 * Handles the back button click event and navigates accordingly
	 * If in embedded mode, it closes the WebComponentManagerService
	 * If the redirect URL is set, it navigates to that URL
	 * If in standalone mode without a redirect URL, it navigates to the rooms page
	 */
	async goBack() {
		if (this.appDataService.isEmbeddedMode()) {
			this.wcManagerService.close();
		}

		const redirectTo = this.navigationService.getLeaveRedirectURL();
		if (redirectTo) {
			// Navigate to the specified redirect URL
			await this.navigationService.redirectTo(redirectTo);
			return;
		}

		if (this.appDataService.isStandaloneMode()) {
			// Navigate to rooms page
			await this.navigationService.navigateTo('/rooms');
		}
	}

	async submitAccessMeeting() {
		const { valid, value } = this.participantForm;
		if (!valid || !value.name?.trim()) {
			// If the form is invalid, do not proceed
			console.warn('Participant form is invalid. Cannot access meeting.');
			return;
		}

		this.participantName = value.name.trim();

		try {
			await this.generateParticipantToken();
			await this.addParticipantNameToUrl();
			await this.roomService.loadRoomPreferences(this.roomId);
			this.showMeeting = true;

			// Subscribe to remote participants updates
			this.componentParticipantService.remoteParticipants$
				.pipe(takeUntil(this.destroy$))
				.subscribe((participants) => {
					this.remoteParticipants = participants as CustomParticipantModel[];
				});
			this.componentParticipantService.localParticipant$
				.pipe(takeUntil(this.destroy$))
				.subscribe((participant) => {
					this.localParticipant = participant as CustomParticipantModel;
				});
		} catch (error) {
			console.error('Error accessing meeting:', error);
		}
	}

	/**
	 * Generates a participant token for joining a meeting.
	 *
	 * @throws When participant already exists in the room (status 409)
	 * @returns Promise that resolves when token is generated
	 */
	private async generateParticipantToken() {
		try {
			this.participantToken = await this.participantService.generateToken({
				roomId: this.roomId,
				secret: this.roomSecret,
				participantName: this.participantName
			});
			this.participantName = this.participantService.getParticipantName()!;
		} catch (error: any) {
			console.error('Error generating participant token:', error);
			switch (error.status) {
				case 400:
					// Invalid secret
					await this.navigationService.redirectToErrorPage(ErrorReason.INVALID_ROOM_SECRET, true);
					break;
				case 404:
					// Room not found
					await this.navigationService.redirectToErrorPage(ErrorReason.INVALID_ROOM, true);
					break;
				case 409:
					// Room is closed
					await this.navigationService.redirectToErrorPage(ErrorReason.CLOSED_ROOM, true);
					break;
				default:
					await this.navigationService.redirectToErrorPage(ErrorReason.INTERNAL_ERROR, true);
			}

			throw new Error('Error generating participant token');
		}
	}

	/**
	 * Add participant name as a query parameter to the URL
	 */
	private async addParticipantNameToUrl() {
		await this.navigationService.updateQueryParamsFromUrl(this.route.snapshot.queryParams, {
			'participant-name': this.participantName
		});
	}

	onRoomCreated(room: Room) {
		room.on(
			RoomEvent.DataReceived,
			async (payload: Uint8Array, _participant?: RemoteParticipant, _kind?: DataPacket_Kind, topic?: string) => {
				const event = JSON.parse(new TextDecoder().decode(payload));

				switch (topic) {
					case 'recordingStopped': {
						// If a 'recordingStopped' event is received and there was no previous recordings,
						// update the hasRecordings flag and refresh the recording token
						if (this.hasRecordings) return;

						this.hasRecordings = true;

						try {
							await this.recordingService.generateRecordingToken(this.roomId, this.roomSecret);
						} catch (error) {
							console.error('Error refreshing recording token:', error);
						}

						break;
					}
					case MeetSignalType.MEET_ROOM_PREFERENCES_UPDATED: {
						// Update room preferences
						const { preferences } = event as MeetRoomPreferencesUpdatedPayload;
						this.featureConfService.setRoomPreferences(preferences);

						// Refresh recording token if recording is enabled
						if (preferences.recordingPreferences.enabled) {
							try {
								await this.recordingService.generateRecordingToken(this.roomId, this.roomSecret);
							} catch (error) {
								console.error('Error refreshing recording token:', error);
							}
						}
						break;
					}
					case MeetSignalType.MEET_PARTICIPANT_ROLE_UPDATED: {
						// Update participant role
						const { participantIdentity, newRole, secret } = event as MeetParticipantRoleUpdatedPayload;

						if (participantIdentity === this.localParticipant!.identity) {
							if (!secret) return;

							this.roomSecret = secret;
							this.roomService.setRoomSecret(secret, false);

							try {
								await this.participantService.refreshParticipantToken({
									roomId: this.roomId,
									secret,
									participantName: this.participantName,
									participantIdentity
								});

								this.localParticipant!.meetRole = newRole;
								this.notificationService.showSnackbar(`You have been assigned the role of ${newRole}`);
							} catch (error) {
								console.error('Error refreshing participant token to update role:', error);
							}
						} else {
							const participant = this.remoteParticipants.find((p) => p.identity === participantIdentity);
							if (participant) {
								participant.meetRole = newRole;
							}
						}

						break;
					}
				}
			}
		);
	}

	onParticipantConnected(event: ParticipantModel) {
		const message: WebComponentOutboundEventMessage<WebComponentEvent.JOINED> = {
			event: WebComponentEvent.JOINED,
			payload: {
				roomId: event.getProperties().room?.name || '',
				participantIdentity: event.identity
			}
		};
		this.wcManagerService.sendMessageToParent(message);
	}

	async onParticipantLeft(event: ParticipantLeftEvent) {
		let leftReason = this.getReasonParamFromEvent(event.reason);
		if (leftReason === LeftEventReason.MEETING_ENDED && this.meetingEndedByMe) {
			leftReason = LeftEventReason.MEETING_ENDED_BY_SELF;
		}

		// Send LEFT event to the parent component
		const message: WebComponentOutboundEventMessage<WebComponentEvent.LEFT> = {
			event: WebComponentEvent.LEFT,
			payload: {
				roomId: event.roomName,
				participantIdentity: event.participantName,
				reason: leftReason
			}
		};
		this.wcManagerService.sendMessageToParent(message);

		// Remove the moderator secret from session storage if the participant left for a reason other than browser unload
		if (event.reason !== ParticipantLeftReason.BROWSER_UNLOAD) {
			this.sessionStorageService.removeRoomSecret(event.roomName);
		}

		// Navigate to the disconnected page with the reason
		await this.navigationService.navigateTo('disconnected', { reason: leftReason }, true);
	}

	/**
	 * Maps ParticipantLeftReason to LeftEventReason.
	 * This method translates the technical reasons for a participant leaving the room
	 * into user-friendly reasons that can be used in the UI or for logging purposes.
	 * @param reason The technical reason for the participant leaving the room.
	 * @returns The corresponding LeftEventReason.
	 */
	private getReasonParamFromEvent(reason: ParticipantLeftReason): LeftEventReason {
		const reasonMap: Record<ParticipantLeftReason, LeftEventReason> = {
			[ParticipantLeftReason.LEAVE]: LeftEventReason.VOLUNTARY_LEAVE,
			[ParticipantLeftReason.BROWSER_UNLOAD]: LeftEventReason.VOLUNTARY_LEAVE,
			[ParticipantLeftReason.NETWORK_DISCONNECT]: LeftEventReason.NETWORK_DISCONNECT,
			[ParticipantLeftReason.SIGNAL_CLOSE]: LeftEventReason.NETWORK_DISCONNECT,
			[ParticipantLeftReason.SERVER_SHUTDOWN]: LeftEventReason.SERVER_SHUTDOWN,
			[ParticipantLeftReason.PARTICIPANT_REMOVED]: LeftEventReason.PARTICIPANT_KICKED,
			[ParticipantLeftReason.ROOM_DELETED]: LeftEventReason.MEETING_ENDED,
			[ParticipantLeftReason.DUPLICATE_IDENTITY]: LeftEventReason.UNKNOWN,
			[ParticipantLeftReason.OTHER]: LeftEventReason.UNKNOWN
		};
		return reasonMap[reason] ?? LeftEventReason.UNKNOWN;
	}

	async leaveMeeting() {
		await this.openviduService.disconnectRoom();
	}

	async endMeeting() {
		if (!this.participantService.isModeratorParticipant()) return;

		this.meetingEndedByMe = true;

		try {
			await this.meetingService.endMeeting(this.roomId);
		} catch (error) {
			console.error('Error ending meeting:', error);
			this.notificationService.showSnackbar('Failed to end meeting');
		}
	}

	async kickParticipant(participant: CustomParticipantModel) {
		if (!this.participantService.isModeratorParticipant()) return;

		try {
			await this.meetingService.kickParticipant(this.roomId, participant.identity);
		} catch (error) {
			console.error('Error kicking participant:', error);
			this.notificationService.showSnackbar('Failed to kick participant');
		}
	}

	/**
	 * Makes a participant as moderator.
	 * @param participant The participant to make as moderator.
	 */
	async makeModerator(participant: CustomParticipantModel) {
		if (!this.participantService.isModeratorParticipant()) return;

		try {
			await this.meetingService.changeParticipantRole(
				this.roomId,
				participant.identity,
				ParticipantRole.MODERATOR
			);
		} catch (error) {
			console.error('Error making participant moderator:', error);
			this.notificationService.showSnackbar('Failed to make participant moderator');
		}
	}

	/**
	 * Unmakes a participant as moderator.
	 * @param participant The participant to unmake as moderator.
	 */
	async unmakeModerator(participant: CustomParticipantModel) {
		if (!this.participantService.isModeratorParticipant()) return;

		try {
			await this.meetingService.changeParticipantRole(this.roomId, participant.identity, ParticipantRole.SPEAKER);
		} catch (error) {
			console.error('Error unmaking participant moderator:', error);
			this.notificationService.showSnackbar('Failed to unmake participant moderator');
		}
	}

	async copyModeratorLink() {
		this.clipboard.copy(this.room!.moderatorUrl);
		this.notificationService.showSnackbar('Moderator link copied to clipboard');
	}

	async copySpeakerLink() {
		this.clipboard.copy(this.room!.speakerUrl);
		this.notificationService.showSnackbar('Speaker link copied to clipboard');
	}

	async onRecordingStartRequested(event: RecordingStartRequestedEvent) {
		try {
			await this.recordingService.startRecording(event.roomName);
		} catch (error: unknown) {
			if ((error as any).status === 503) {
				console.error(
					`No egress service was able to register a request.
Check your CPU usage or if there's any Media Node with enough CPU.
Remember that by default, a recording uses 4 CPUs for each room.`
				);
			} else {
				console.error(error);
			}
		}
	}

	async onRecordingStopRequested(event: RecordingStopRequestedEvent) {
		try {
			await this.recordingService.stopRecording(event.recordingId);
		} catch (error) {
			console.error(error);
		}
	}

	async onViewRecordingsClicked() {
		window.open(`/room/${this.roomId}/recordings?secret=${this.roomSecret}`, '_blank');
	}
}
