import { Injectable, Signal, WritableSignal, signal } from '@angular/core';
import { toObservable } from '@angular/core/rxjs-interop';
import {
	AudioCaptureOptions,
	DataPublishOptions,
	LocalParticipant,
	LocalTrackPublication,
	Participant,
	RemoteParticipant,
	ScreenShareCaptureOptions,
	Track,
	VideoCaptureOptions,
	VideoPresets
} from 'livekit-client';
import { Observable } from 'rxjs';
import { ILogger } from '../../models/logger.model';
import { ParticipantModel, ParticipantProperties } from '../../models/participant.model';
import { OpenViduComponentsConfigService } from '../config/directive-config.service';
import { GlobalConfigService } from '../config/global-config.service';
import { E2eeService } from '../e2ee/e2ee.service';
import { LoggerService } from '../logger/logger.service';
import { OpenViduService } from '../openvidu/openvidu.service';
import { StorageService } from '../storage/storage.service';

@Injectable({
	providedIn: 'root'
})
export class ParticipantService {
	/**
	 * Local participant Observable which pushes the local participant object in every update.
	 * @deprecated Please prefer `localParticipantSignal` for reactive updates and `localParticipant$` when using RxJS.
	 */
	localParticipant$: Observable<ParticipantModel | undefined>;

	/**
	 * Remote participants Observable which pushes the remote participants array in every update.
	 * @deprecated Please prefer `remoteParticipantsSignal` for reactive updates and `remoteParticipants$` when using RxJS.
	 */
	remoteParticipants$: Observable<ParticipantModel[]>;

	/**
	 * Local participant Signal for reactive programming with Angular signals.
	 * This is a modern alternative to localParticipant$ Observable.
	 * @since Angular 16+
	 */
	localParticipantSignal: Signal<ParticipantModel | undefined>;
	private localParticipantWritableSignal: WritableSignal<ParticipantModel | undefined> = signal<ParticipantModel | undefined>(undefined);

	/**
	 * Remote participants Signal for reactive programming with Angular signals.
	 * This is a modern alternative to remoteParticipants$ Observable.
	 * @since Angular 16+
	 */
	remoteParticipantsSignal: Signal<ParticipantModel[]>;
	private remoteParticipantsWritableSignal: WritableSignal<ParticipantModel[]> = signal<ParticipantModel[]>([]);

	private localParticipant: ParticipantModel | undefined;
	private lastLocalParticipantSnapshot: ParticipantModel | undefined;
	private remoteParticipants: ParticipantModel[] = [];
	private log: ILogger;

	/**
	 * @internal
	 */
	constructor(
		private globalService: GlobalConfigService,
		private directiveService: OpenViduComponentsConfigService,
		private openviduService: OpenViduService,
		private storageSrv: StorageService,
		private loggerSrv: LoggerService,
		private e2eeService: E2eeService
	) {
		this.log = this.loggerSrv.get('ParticipantService');

		// Expose readonly signals
		this.localParticipantSignal = this.localParticipantWritableSignal.asReadonly();
		this.remoteParticipantsSignal = this.remoteParticipantsWritableSignal.asReadonly();

		// Create Observables from signals for backward compatibility
		this.localParticipant$ = toObservable(this.localParticipantWritableSignal);
		this.remoteParticipants$ = toObservable(this.remoteParticipantsWritableSignal);
	}

	/**
	 * @internal
	 */
	clear(): void {
		this.localParticipant = undefined;
		this.lastLocalParticipantSnapshot = undefined;
		this.remoteParticipants = [];
		this.localParticipantWritableSignal.set(undefined);
		this.remoteParticipantsWritableSignal.set([]);
	}

	/**
	 * @internal
	 * Setting up the local participant object.
	 * @param participant
	 */
	setLocalParticipant(participant: LocalParticipant) {
		const room = this.openviduService.getRoom();
		const fallbackName = this.storageSrv.getParticipantName() || undefined;
		this.localParticipant = this.newParticipant({
			participant,
			room,
			fallbackName,
			preferredNameResolver: () => this.openviduService.getPreferredLocalParticipantName()
		});
	}

	/**
	 * Returns the local participant object.
	 *
	 * @deprecated Please prefer `localParticipantSignal()` for reactive updates and `localParticipant$` when using RxJS.
	 */
	getLocalParticipant(): ParticipantModel | undefined {
		return this.localParticipant;
	}

	/**
	 * Connects to the room and publishes the local tracks.
	 * @internal
	 */
	async connect(): Promise<void> {
		let isCameraEnabled: boolean = this.isMyCameraEnabled();
		let isMicrophoneEnabled: boolean = this.isMyMicrophoneEnabled();
		let prejoinTracks = this.openviduService.getLocalTracks();

		if (prejoinTracks.length === 0 && (isCameraEnabled || isMicrophoneEnabled)) {
			prejoinTracks = await this.openviduService.createLocalTracks(isCameraEnabled, isMicrophoneEnabled);
		}

		await this.openviduService.connectRoom();
		this.setLocalParticipant(this.openviduService.getRoom().localParticipant);

		const videoTrack = prejoinTracks.find((track) => track.kind === Track.Kind.Video);
		const audioTrack = prejoinTracks.find((track) => track.kind === Track.Kind.Audio);

		const promises: Promise<LocalTrackPublication>[] = [];
		if (this.localParticipant && videoTrack) {
			promises.push(this.localParticipant.publishTrack(videoTrack));
		}
		if (this.localParticipant && audioTrack) {
			promises.push(this.localParticipant?.publishTrack(audioTrack));
		}

		await Promise.all(promises);
		this.updateLocalParticipant();
		// if(!isCameraEnabled) await this.setCameraEnabled(isCameraEnabled);
		// if(!isMicrophoneEnabled) await this.setMicrophoneEnabled(isMicrophoneEnabled);
		// Once the Room is created, the temporary tracks are not longer needed.
		this.log.d('Connected to room', this.openviduService.getRoom());
		this.openviduService.getRoom().remoteParticipants.forEach((p) => {
			this.addRemoteParticipant(p);
		});
	}

	/**
	 * Publishes a new data payload to the room. Data will be forwarded to each participant in the room if the destination field in publishOptions is empty.
	 * @param data
	 * @param {DataPublishOptions} publishOptions [DataPublishOptions](https://docs.livekit.io/client-sdk-js/types/DataPublishOptions.html)
	 */
	publishData(data: Uint8Array, publishOptions: DataPublishOptions): Promise<void> {
		if (this.localParticipant) {
			return this.localParticipant.publishData(data, publishOptions);
		}
		return Promise.reject('Local participant not found');
	}

	/**
	 * Switches the active camera track used in this room to the given device id.
	 * @param deviceId
	 */
	async switchCamera(deviceId: string): Promise<void> {
		if (this.openviduService.isRoomConnected()) {
			await this.localParticipant?.switchCamera(deviceId);
		} else {
			await this.openviduService.switchCamera(deviceId);
		}
		// this.updateLocalParticipant();
	}

	/**
	 * Switches the active microphone track used in this room to the given device id.
	 * @param deviceId
	 */
	async switchMicrophone(deviceId: string): Promise<void> {
		if (this.openviduService.isRoomConnected()) {
			await this.localParticipant?.switchMicrophone(deviceId);
		} else {
			await this.openviduService.switchMicrophone(deviceId);
		}
		// this.updateLocalParticipant();
	}

	/**
	 * Switches the active screen share track showing a native browser dialog to select a screen or window.
	 */
	async switchScreenShare(): Promise<void> {
		if (!this.localParticipant) {
			this.log.e('Local participant is undefined when switching screenshare');
			return;
		}

		// Chrome / Safari: seamless replaceTrack keeps the same publication SID.
		const options = this.getScreenCaptureOptions();
		const [newTrack] = await this.localParticipant.createScreenTracks(options);
		if (newTrack) {
			newTrack.addListener('ended', async () => {
				this.log.d('Clicked native stop button. Stopping screen sharing');
				await this.setScreenShareEnabled(false);
			});

			try {
				await this.localParticipant.switchScreenshare(newTrack);
			} catch (error) {
				newTrack.stop();
				throw error;
			}
		}

		// this.updateLocalParticipant();
	}

	/**
	 * Sets the local participant camera enabled or disabled.
	 * @param enabled
	 */
	async setCameraEnabled(enabled: boolean): Promise<void> {
		if (this.openviduService.isRoomConnected()) {
			const storageDevice = this.storageSrv.getVideoDevice();
			let options: VideoCaptureOptions | undefined;
			if (storageDevice) {
				options = {
					deviceId: storageDevice.device,
					facingMode: 'user',
					resolution: VideoPresets.h720.resolution
				};
			}
			await this.localParticipant?.setCameraEnabled(enabled, options);
			this.updateLocalParticipant();
		} else {
			await this.openviduService.setVideoTrackEnabled(enabled);
		}
	}

	/**
	 * Sets the local participant microphone enabled or disabled.
	 * @param enabled
	 */
	async setMicrophoneEnabled(enabled: boolean): Promise<void> {
		if (this.openviduService.isRoomConnected()) {
			const storageDevice = this.storageSrv.getAudioDevice();
			let options: AudioCaptureOptions | undefined;
			if (storageDevice) {
				options = {
					deviceId: storageDevice.device
				};
			}
			await this.localParticipant?.setMicrophoneEnabled(enabled, options);
			this.updateLocalParticipant();
		} else {
			this.openviduService.setAudioTrackEnabled(enabled);
		}
	}

	/**
	 * Share or unshare the local participant screen.
	 * @param enabled: true to share the screen, false to unshare it
	 *
	 */
	async setScreenShareEnabled(enabled: boolean): Promise<void> {
		const options = this.getScreenCaptureOptions();
		const track = await this.localParticipant?.setScreenShareEnabled(enabled, options);
		if (enabled && track) {
			// Set all videos to normal size when a local screen is shared
			this.resetRemoteStreamsToNormalSize();
			this.resetMyStreamsToNormalSize();
			this.localParticipant?.toggleVideoPinned(track.trackSid);
			this.localParticipant?.setScreenTrackPublicationDate(track.trackSid, new Date().getTime());

			track?.addListener('ended', async () => {
				this.log.d('Clicked native stop button. Stopping screen sharing');
				await this.setScreenShareEnabled(false);
			});
		} else if (!enabled && track) {
			// Enlarge the last screen shared when a local screen is stopped
			this.localParticipant?.setScreenTrackPublicationDate(track.trackSid, -1);
			this.resetRemoteStreamsToNormalSize();
			this.resetMyStreamsToNormalSize();
			this.setLastScreenPinned();
		}
		this.updateLocalParticipant();
	}

	/**
	 * @internal
	 * As updating name requires that the participant has the `canUpdateOwnMetadata` to true in server side, which is a little bit insecure,
	 * we decided to not allow this feature for now.
	 */
	// setMyName(name: string) {
	// 	if (!this.localParticipant) return;
	// 	this.localParticipant.setName(name);
	// 	this.updateLocalParticipant();
	// }

	/**
	 * Sets as speaking to all participants given in the array.
	 * @param speakers
	 * @internal
	 */
	setSpeaking(speakers: Participant[]) {
		// Set all participants' isSpeaking property to false
		this.localParticipant?.setSpeaking(false);
		this.remoteParticipants.forEach((participant) => participant.setSpeaking(false));
		speakers.forEach((s) => {
			if (s.isLocal) {
				this.localParticipant?.setSpeaking(true);
			} else {
				const participant = this.remoteParticipants.find((p) => p.sid === s.sid);
				participant?.setSpeaking(true);
				this.updateRemoteParticipants();
			}
		});
		this.updateLocalParticipant();
	}

	/**
	 * Sets the encryption error state for a participant.
	 * This is called when a participant cannot decrypt video streams due to an incorrect encryption key.
	 * @param participantSid - The SID of the participant with the encryption error
	 * @param hasError - Whether the participant has an encryption error
	 * @internal
	 */
	setEncryptionError(participantSid: string, hasError: boolean) {
		if (this.localParticipant?.sid === participantSid) {
			this.localParticipant.setEncryptionError(hasError);
			this.updateLocalParticipant();
		} else {
			const participant = this.remoteParticipants.find((p) => p.sid === participantSid);
			if (participant) {
				participant.setEncryptionError(hasError);
				this.updateRemoteParticipants();
			}
		}
	}

	/**
	 * Returns the local participant name.
	 */
	getMyName(): string | undefined {
		return this.localParticipant?.name;
	}

	/**
	 * @internal
	 */
	toggleMyVideoPinned(sid: string | undefined) {
		if (sid && this.localParticipant) this.localParticipant.toggleVideoPinned(sid);
		this.updateLocalParticipant();
	}

	/**
	 * @internal
	 */
	toggleMyVideoMinimized(sid: string | undefined) {
		if (sid && this.localParticipant) this.localParticipant.toggleVideoMinimized(sid);
		this.updateLocalParticipant();
	}

	/**
	 * @internal
	 */
	resetMyStreamsToNormalSize() {
		this.localParticipant?.setAllVideoPinned(false);
		// 	this.updateLocalParticipant();
	}

	/**
	 * Returns if the local participant camera is enabled.
	 */
	isMyCameraEnabled(): boolean {
		if (this.openviduService.isRoomConnected() && this.localParticipant) {
			return this.localParticipant.isCameraEnabled;
		} else {
			const directiveCameraEnabled = this.directiveService.isVideoEnabled();

			if (!directiveCameraEnabled) {
				return false;
			}
			return this.openviduService.isVideoTrackEnabled() && this.storageSrv.isCameraEnabled();
		}
	}

	/**
	 * Returns if the local participant microphone is enabled.
	 */
	isMyMicrophoneEnabled(): boolean {
		if (this.openviduService.isRoomConnected() && this.localParticipant) {
			return this.localParticipant.isMicrophoneEnabled;
		} else {
			const directiveMicropgoneEnabled = this.directiveService.isAudioEnabled();

			if (!directiveMicropgoneEnabled) {
				return false;
			}
			return this.openviduService.isAudioTrackEnabled() && this.storageSrv.isMicrophoneEnabled();
		}
	}

	/**
	 * Returns if the local participant screen is enabled.
	 */
	isMyScreenShareEnabled(): boolean {
		return this.localParticipant?.isScreenShareEnabled || false;
	}

	/**
	 * Forces to update the local participant object and fire a new `localParticipant$` Observable event.
	 */
	updateLocalParticipant() {
		const localParticipantFromSignal = this.localParticipantWritableSignal();

		// Backward compatibility: if the consumer mutated the last emitted snapshot and
		// then calls updateLocalParticipant(), keep the internal source of truth in sync.
		// Only sync when the emitted snapshot actually diverged after emission; otherwise,
		// a stale observable/signal snapshot would overwrite newer canonical state.
		if (
			this.localParticipant &&
			localParticipantFromSignal &&
			this.lastLocalParticipantSnapshot &&
			localParticipantFromSignal !== this.localParticipant &&
			localParticipantFromSignal.sid === this.localParticipant.sid &&
			this.hasParticipantSnapshotMutations(localParticipantFromSignal, this.lastLocalParticipantSnapshot)
		) {
			Object.assign(this.localParticipant, localParticipantFromSignal);
		}

		// Update Signal - create new reference to trigger reactivity
		// The Observable will automatically emit via toObservable()
		if (this.localParticipant) {
			const updatedParticipant = this.cloneParticipant(this.localParticipant);
			this.localParticipantWritableSignal.set(updatedParticipant);
			this.lastLocalParticipantSnapshot = this.cloneParticipant(updatedParticipant);
		} else {
			this.localParticipantWritableSignal.set(undefined);
			this.lastLocalParticipantSnapshot = undefined;
		}
	}

	private hasParticipantSnapshotMutations(current: ParticipantModel, previous: ParticipantModel): boolean {
		const currentState = current as unknown as Record<string, unknown>;
		const previousState = previous as unknown as Record<string, unknown>;
		const keys = new Set([...Object.keys(currentState), ...Object.keys(previousState)]);

		for (const key of keys) {
			if (!Object.is(currentState[key], previousState[key])) {
				return true;
			}
		}

		return false;
	}

	private cloneParticipant<T extends ParticipantModel>(participant: T): T {
		return Object.assign(Object.create(Object.getPrototypeOf(participant)), {
			...participant
		});
	}

	/**
	 * Sets the last screen element as pinned
	 * @internal
	 */
	setLastScreenPinned() {
		if (!this.localParticipant?.isScreenShareEnabled && !this.someRemoteIsSharingScreen()) {
			return; // Exit early if neither local nor remote participants are sharing screen
		}
		let localCreatedAt = -Infinity;
		let localTrackSid = '';
		if (this.localParticipant?.isScreenShareEnabled) {
			localCreatedAt = Math.max(...this.localParticipant.screenTrackPublicationDate.values());
			this.localParticipant.screenTrackPublicationDate.forEach((value, key) => {
				if (value === localCreatedAt) {
					localTrackSid = key;
					return;
				}
			});
		}

		let remoteCreatedAt = -Infinity;
		let remoteTrackSid = '';
		if (this.someRemoteIsSharingScreen()) {
			const lastRemoteParticipant = this.remoteParticipants.reduce((prev, current) => {
				const prevMax = Math.max(...prev.screenTrackPublicationDate.values());
				const currentMax = Math.max(...current.screenTrackPublicationDate.values());
				return prevMax > currentMax ? prev : current;
			});
			remoteCreatedAt = Math.max(...lastRemoteParticipant.screenTrackPublicationDate.values());
			lastRemoteParticipant.screenTrackPublicationDate.forEach((value, key) => {
				if (value === remoteCreatedAt) {
					remoteTrackSid = key;
					return;
				}
			});
		}

		if (remoteCreatedAt > localCreatedAt) {
			this.toggleRemoteVideoPinned(remoteTrackSid);
		} else {
			this.toggleMyVideoPinned(localTrackSid);
		}
	}

	/**
	 * Returns the participant with the given identity.
	 * @param identity
	 * @returns
	 */
	getParticipantByIdentity(identity: string): ParticipantModel | undefined {
		if (this.localParticipant?.identity === identity) {
			return this.localParticipant;
		}
		return this.remoteParticipants.find((p) => p.identity === identity);
	}

	/* ------------------------------ Remote Participants ------------------------------ */

	/**
	 * Returns all remote participants in the room.
	 *
	 * @deprecated Please prefer `remoteParticipantsSignal()` for automatic reactive updates or `remoteParticipants$` when using Observables.
	 */
	getRemoteParticipants(): ParticipantModel[] {
		return this.remoteParticipants;
	}

	/**
	 * Returns the remote participant with the given sid.
	 * @param sid
	 */
	getRemoteParticipantBySid(sid: string): ParticipantModel | undefined {
		return this.remoteParticipants.find((p) => p.sid === sid);
	}

	/**
	 * Force to update the remote participants object and fire a new `remoteParticipants$` Observable event.
	 */
	updateRemoteParticipants() {
		// Update Signal - create new array reference to trigger reactivity
		// The Observable will automatically emit via toObservable()
		this.remoteParticipantsWritableSignal.set([...this.remoteParticipants]);
	}

	/**
	 * @internal
	 */
	addRemoteParticipant(participant: RemoteParticipant) {
		const index = this.remoteParticipants.findIndex((p) => p.sid === participant.sid);
		if (index >= 0) {
			const remoteParticipant = this.remoteParticipants[index];
			const pp: ParticipantProperties = remoteParticipant.getProperties();
			pp.participant = participant;
			this.remoteParticipants[index] = this.newParticipant(pp);
		} else {
			this.remoteParticipants.push(this.newParticipant({ participant }));
		}
		this.updateRemoteParticipants();
	}

	/**
	 * Removes participant track from the remote participant object.
	 * @param participant
	 * @param trackSid
	 * @internal
	 */
	removeRemoteParticipantTrack(participant: RemoteParticipant, trackSid: string) {
		const index = this.remoteParticipants.findIndex((p) => p.sid === participant.sid);
		if (index >= 0) {
			const track = this.remoteParticipants[index].tracks.find((t) => t.trackSid === trackSid);
			track?.track?.stop();
			track?.track?.detach();
			const pp: ParticipantProperties = this.remoteParticipants[index].getProperties();
			pp.participant = participant;
			this.remoteParticipants[index] = this.newParticipant(pp);
			this.updateRemoteParticipants();
		}
	}

	/**
	 * @internal
	 */
	removeRemoteParticipant(sid: string) {
		const index = this.remoteParticipants.findIndex((p) => p.sid === sid);
		if (index !== -1) {
			this.remoteParticipants.splice(index, 1);
			this.updateRemoteParticipants();
		}
	}

	/**
	 * @internal
	 */
	resetRemoteStreamsToNormalSize() {
		this.remoteParticipants.forEach((participant) => participant.setAllVideoPinned(false));
		// this.updateRemoteParticipants();
	}

	/**
	 * Set the screen track publication date of a remote participant with the aim of taking control of the last screen published
	 * @param participantSid
	 * @param trackSid
	 * @param createdAt
	 * @internal
	 */
	setScreenTrackPublicationDate(participantSid: string, trackSid: string, createdAt: number) {
		const participant = this.remoteParticipants.find((p) => p.sid === participantSid);
		if (participant) {
			participant.setScreenTrackPublicationDate(trackSid, createdAt);
		}
	}

	/**
	 * @internal
	 */
	someRemoteIsSharingScreen(): boolean {
		return this.remoteParticipants.some((p) => p.isScreenShareEnabled);
	}

	/**
	 * @internal
	 */
	toggleRemoteVideoPinned(sid: string | undefined) {
		if (sid) {
			const participant = this.remoteParticipants.find((p) => p.tracks.some((track) => track.trackSid === sid));
			if (participant) {
				participant.toggleVideoPinned(sid);
			}
			this.updateRemoteParticipants();
		}
	}

	/**
	 * Sets the remote participant video track element muted or unmuted .
	 * @internal
	 */
	setRemoteMutedForcibly(sid: string, value: boolean) {
		const p = this.getRemoteParticipantBySid(sid);
		if (p) {
			p.setMutedForcibly(value);
			this.updateRemoteParticipants();
		}
	}

	private newParticipant(props: ParticipantProperties): ParticipantModel {
		let participant: ParticipantModel;
		if (this.globalService.hasParticipantFactory()) {
			participant = this.globalService.getParticipantFactory().apply(this, [props]);
		} else {
			participant = new ParticipantModel(props);
		}

		// Decrypt participant name asynchronously if E2EE is enabled
		this.decryptParticipantName(participant);

		return participant;
	}

	/**
	 * Decrypts the participant name if E2EE is enabled.
	 * Updates the participant model asynchronously.
	 * @param participant - The participant model to decrypt the name for
	 * @private
	 */
	private async decryptParticipantName(participant: ParticipantModel): Promise<void> {
		const originalName = participant.name;
		if (!originalName) {
			return;
		}

		try {
			const decryptedName = await this.e2eeService.decryptOrMask(originalName, participant.identity);
			participant.setDecryptedName(decryptedName);

			// Update observables to reflect the decrypted name
			if (participant.isLocal) {
				this.updateLocalParticipant();
			} else {
				this.updateRemoteParticipants();
			}
		} catch (error) {
			this.log.w('Failed to decrypt participant name:', error);
		}
	}

	private getScreenCaptureOptions(): ScreenShareCaptureOptions {
		return {
			audio: true,
			video: {
				displaySurface: 'browser' // Set browser tab as default options
			},
			systemAudio: 'include', // Include system audio as an option
			resolution: VideoPresets.h1080.resolution,
			contentHint: 'text', // Optimized for detailed content, adjust based on use case
			suppressLocalAudioPlayback: true, // Prevent echo by not playing local audio
			selfBrowserSurface: 'exclude', // Avoid self capture to prevent mirror effect
			surfaceSwitching: 'include', // Allow users to switch shared tab dynamically
			preferCurrentTab: false // Do not force current tab to be selected
		};
	}
}
