import { BSVersion } from "shared/bs-version.interface";
import { BehaviorSubject, Observable, Subscription, lastValueFrom, map, share, shareReplay, throwError } from "rxjs";
import { IpcService } from "./ipc.service";
import { ModalExitCode, ModalService } from "./modale.service";
import { NotificationService } from "./notification.service";
import { ProgressBarService } from "./progress-bar.service";
import { EditVersionModal } from "renderer/components/modal/modal-types/edit-version-modal.component";
import { popElement } from "shared/helpers/array.helpers";
import { ImportVersionModal } from "renderer/components/modal/modal-types/import-version-modal.component";
import { Progression } from "main/helpers/fs.helpers";
import { CustomError } from "shared/models/exceptions/custom-error.class";

export class BSVersionManagerService {
    private static instance: BSVersionManagerService;

    private readonly ipcService: IpcService;
    private readonly modalService: ModalService;
    private readonly notification: NotificationService;
    private readonly progressBar: ProgressBarService;
    private readonly modals: ModalService;

    private askInstalledVersionsObserver$: Observable<BSVersion[]> | null = null;
    public readonly installedVersions$: BehaviorSubject<BSVersion[]> = new BehaviorSubject([]);
    public readonly availableVersions$: BehaviorSubject<BSVersion[]> = new BehaviorSubject([]);

    private constructor() {
        this.ipcService = IpcService.getInstance();
        this.modalService = ModalService.getInstance();
        this.notification = NotificationService.getInstance();
        this.progressBar = ProgressBarService.getInstance();
        this.modals = ModalService.getInstance();

        this.askAvailableVersions();
        this.askInstalledVersions();
    }

    public static getInstance() {
        if (!BSVersionManagerService.instance) {
            BSVersionManagerService.instance = new BSVersionManagerService();
        }
        return BSVersionManagerService.instance;
    }

    public setInstalledVersions(versions: BSVersion[]) {
        const sorted = BSVersionManagerService.sortVersions(versions);
        this.installedVersions$.next(BSVersionManagerService.removeDuplicateVersions(sorted));
    }

    public getInstalledVersions(): BSVersion[] {
        return this.installedVersions$.value;
    }

    public askAvailableVersions(): Promise<BSVersion[]> {
        return lastValueFrom(this.ipcService.sendV2("bs-version.get-version-dict")).then(res => {
            this.availableVersions$.next(res);
            return res;
        });
    }

    public async askInstalledVersions(): Promise<BSVersion[]> {
        if (this.askInstalledVersionsObserver$) {
            return lastValueFrom(this.askInstalledVersionsObserver$);
        }

        this.askInstalledVersionsObserver$ = this.ipcService
            .sendV2("bs-version.installed-versions")
            .pipe(
                map(versions => {
                    let processed = BSVersionManagerService.sortVersions(versions);
                    processed = BSVersionManagerService.removeDuplicateVersions(processed);
                    return processed;
                }),
                share(),
            );

        return lastValueFrom(this.askInstalledVersionsObserver$)
            .then(versions => {
                this.setInstalledVersions(versions);
                return versions;
            })
            .finally(() => {
                this.askInstalledVersionsObserver$ = null;
            });
    }

    public async isVersionInstalled(version: BSVersion): Promise<boolean> {
        try {
            const versions: BSVersion[] = this.askInstalledVersionsObserver$
                ? await lastValueFrom(this.askInstalledVersionsObserver$)
                : this.getInstalledVersions();

            return !!versions.find(v =>
                v.BSVersion === version.BSVersion
                && v.name === version.name
                && v.steam === version.steam
                && v.oculus === version.oculus
            );
        } catch (error) {
            return false;
        }
    }

    public async editVersion(version: BSVersion): Promise<BSVersion> {
        const modalRes = await this.modalService.openModal(EditVersionModal, {data: { version, clone: false }});
        if (modalRes.exitCode !== ModalExitCode.COMPLETED) {
            return null;
        }
        if (modalRes.data.name?.length < 2) {
            return null;
        }

        return lastValueFrom(this.ipcService.sendV2("bs-version.edit", { version, name: modalRes.data.name, color: modalRes.data.color })).then(res => {
            this.askInstalledVersions();
            return res;
        }).catch(e => {

            const knownErrorTitlesCodes = ["CantEditSteam", "VersionAlreadExist", "CantRename"];
            const knownErrorMessagesCodes = ["CantEditSteam"];

            if(knownErrorTitlesCodes.includes(e.code)){
                this.notification.notifyError({
                    title: `notifications.custom-version.errors.titles.${e.code}`,
                    desc: knownErrorMessagesCodes.includes(e.code) ? `notifications.custom-version.errors.msg.${e.code}` : null
                });
            } else {
                this.notification.notifyError({ title: `notifications.custom-version.errors.titles.UnknownError` });
            }

            return null;
        })
    }

    public async cloneVersion(version: BSVersion): Promise<BSVersion> {
        if (!this.progressBar.require()) {
            return null;
        }
        const modalRes = await this.modalService.openModal(EditVersionModal, {data: { version, clone: true }});
        if (modalRes.exitCode !== ModalExitCode.COMPLETED) {
            return null;
        }
        if (modalRes.data.name?.length < 2) {
            return null;
        }

        this.progressBar.showFake(0.01);

        return lastValueFrom(this.ipcService.sendV2("bs-version.clone", { version, name: modalRes.data.name, color: modalRes.data.color })).then(res => {
            this.notification.notifySuccess({ title: "notifications.custom-version.success.titles.CloningFinished" });
            this.askInstalledVersions();
            return res;
        }).catch((e: CustomError) => {

            const knownErrorTitlesCodes = ["VersionAlreadExist", "CantClone"];

            if(knownErrorTitlesCodes.includes(e.code)){
                this.notification.notifyError({ title: `notifications.custom-version.errors.titles.${e.code}` });
            } else {
                this.notification.notifyError({ title: `notifications.custom-version.errors.titles.UnknownError` });
            }

            return null;
        }).finally(() => {
            this.progressBar.hide()
        });
    }

    public importVersion(): Observable<Progression<BSVersion>> {
        if(!this.progressBar.require()){
            return throwError(() => new Error("Action already in progress"));
        }

        const obs$ = new Observable<Progression<BSVersion>>(obs => {

            const subs: Subscription[] = [];

            (async () => {

                const resModal = await this.modals.openModal(ImportVersionModal);
                if(resModal.exitCode !== ModalExitCode.COMPLETED){
                    return;
                }

                const store = resModal.data;

                const folderRes = await lastValueFrom(this.ipcService.sendV2("choose-folder"));
                if(!folderRes || folderRes.canceled || !folderRes.filePaths?.length){
                    return;
                }

                const import$ = this.ipcService.sendV2("import-version", {fromPath: folderRes.filePaths.at(0), store});

                subs.push(import$.subscribe(obs));

                await lastValueFrom(import$);

                this.notification.notifySuccess({ title: "notifications.bs-import-version.success.imported.title", duration: 3_000 });

            })().then(() => {
                obs.complete()
            }).catch(err => {
                this.notification.notifyError({ title: "notifications.types.error", desc: "notifications.bs-import-version.errors.import-error.desc" });
                obs.error(err)
            }).finally(() => {
                this.askInstalledVersions();
                this.progressBar.hide();
            });

            return () => {
                subs.forEach(sub => sub.unsubscribe());
            }
        }).pipe(
            shareReplay({ bufferSize: 1, refCount: true })
        );

        this.progressBar.show(obs$);

        return obs$;
    }

    public getVersionPath(version: BSVersion): Observable<string> {
        return this.ipcService.sendV2("get-version-full-path", version);
    }

    public static sortVersions(versions: BSVersion[]): BSVersion[] {
        const steamVersion = popElement(v => v.steam, versions);
        const oculusVersion = popElement(v => v.oculus, versions);

        const compare = (a: BSVersion, b: BSVersion) => ( +b.ReleaseDate - +a.ReleaseDate )

        const sorted: BSVersion[] = versions.sort(compare);

        sorted.unshift(...[steamVersion, oculusVersion].filter(Boolean).sort(compare));

        return sorted;
    }

    public static removeDuplicateVersions(versions: BSVersion[]): BSVersion[] {
        return Array.from(new Map(versions.map(version => ([
            JSON.stringify([
                version.BSVersion,
                version.BSManifest,
                version.name,
                version.steam,
                version.oculus
            ]),
            version
        ]))).values());
    }

    public getRecommendedVersion(): BSVersion | undefined {
        return (this.availableVersions$.value ?? []).find(v => v.recommended);
    }
}
