import { Logger } from "@foxxmd/logging";
import dayjs, { Dayjs } from "dayjs";
import EventEmitter from "events";
import { PlayObject, SourcePlayerObj } from "../../core/Atomic.js";
import { buildTrackString, capitalize } from "../../core/StringUtils.js";
import { isNodeNetworkException } from "../common/errors/NodeErrors.js";
import { FormatPlayObjectOptions, InternalConfigOptional, TimeRangeListensFetcher } from "../common/infrastructure/Atomic.js";
import { LastfmClientConfig } from "../common/infrastructure/config/client/lastfm.js";
import LastfmApiClient, { LastFMIgnoredScrobble, playToClientPayload, formatPlayObj } from "../common/vendor/LastfmApiClient.js";
import { Notifiers } from "../notifier/Notifiers.js";
import AbstractScrobbleClient, { nowPlayingUpdateByPlayDuration, playerInNPPlayingOnlyState } from "./AbstractScrobbleClient.js";
import { findCauseByReference } from "../utils/ErrorUtils.js";
import { createGetScrobblesForTimeRangeFunc } from "../utils/ListenFetchUtils.js";

export default class LastfmScrobbler extends AbstractScrobbleClient {

    api: LastfmApiClient;
    requiresAuth = true;
    requiresAuthInteraction = true;
    upstreamType: string = 'Last.fm';
    getScrobblesForTimeRange: TimeRangeListensFetcher

    declare config: LastfmClientConfig;

    constructor(name: any, config: LastfmClientConfig, options: InternalConfigOptional & {[key: string]: any}, notifier: Notifiers, emitter: EventEmitter, logger: Logger, type = 'lastfm') {
        super(type, name, config, notifier, emitter, logger);
        this.api = new LastfmApiClient(name, config.data, {...options, logger});
        // https://www.last.fm/api/show/user.getRecentTracks
        this.MAX_INITIAL_SCROBBLES_FETCH = 100;
        this.supportsNowPlaying = true;
        // last.fm shows Now Playing for the same time as the duration of the track being submitted
        this.nowPlayingMaxThreshold = nowPlayingUpdateByPlayDuration;
        this.getScrobblesForTimeRange = createGetScrobblesForTimeRangeFunc(this.api, this.api.logger);
    }

    formatPlayObj = (obj: any, options: FormatPlayObjectOptions = {}) => formatPlayObj(obj, options);

    protected async doBuildInitData(): Promise<true | string | undefined> {
        await this.api.initialize();
        return true;
    }

    protected async doCheckConnection(): Promise<true | string | undefined> {
        try {
            await this.api.testConnection();
            return true;
        } catch (e) {
            throw e;
        }
    }

    doAuthentication = async () => {
        try {
            return await this.api.testAuth();
        } catch (e) {
            if(isNodeNetworkException(e)) {
                this.logger.error(`Could not communicate with ${this.upstreamType} API`);
            }
            throw e;
        }
    }

    cleanSourceSearchTitle = (playObj: PlayObject) => {
        const {
            data: {
                track,
            } = {},
        } = playObj;
        return track.toLocaleLowerCase().trim();
    }

    public playToClientPayload(playObject: PlayObject): object {
        return playToClientPayload(playObject);
    }

    doScrobble = async (playObj: PlayObject) => {

        const {
            meta: {
                source,
                newFromSource = false,
            } = {}
        } = playObj;

        const sType = newFromSource ? 'New' : 'Backlog';

        try {
          const respPlay = await this.api.scrobble(playObj);
          if (newFromSource) {
            this.logger.info(`Scrobbled (New)     => (${source}) ${buildTrackString(playObj)}`);
        } else {
            this.logger.info(`Scrobbled (Backlog) => (${source}) ${buildTrackString(playObj)}`);
        }
        return respPlay;
        } catch (e) {
            const ignored = findCauseByReference(e, LastFMIgnoredScrobble);
            if(ignored !== undefined) {
                await this.notifier.notify({title: `Client - ${capitalize(this.type)} - ${this.name} - Scrobble Ignored`, message: `Failed to scrobble => ${buildTrackString(playObj)} | ${e.message}`, priority: 'warn'});
            } else {
                await this.notifier.notify({title: `Client - ${capitalize(this.type)} - ${this.name} - Scrobble Error`, message: `Failed to scrobble => ${buildTrackString(playObj)} | Error: ${e.message}`, priority: 'error'});
            }
            this.logger.error({playInfo: buildTrackString(playObj), payload: playToClientPayload(playObj)}, `Scrobble Error (${sType})`);
            throw e;
        }
    }

    doPlayingNow = async (data: SourcePlayerObj) => {
        // last.fm shows Now Playing for the same time as the duration of the track being submitted
        try {
            return this.api.playingNow(data.play);
        } catch (e) {
            throw e;
        }
    }
}
