// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import {
    IClientContext,
    getClientContext,
    handleCall,
    searchForPlaylists,
} from "../client.js";
import chalk from "chalk";
import {
    AppAgent,
    SessionContext,
    AppAction,
    ActionContext,
    DisplayType,
    AppAgentEvent,
    DynamicDisplay,
    ReadinessReport,
    TypeAgentAction,
    ResolveEntityResult,
} from "@typeagent/agent-sdk";
import { createActionResultFromError } from "@typeagent/agent-sdk/helpers/action";
import { searchTracks } from "../client.js";
import { htmlStatus } from "../playback.js";
import { getPlayerCommandInterface } from "./playerCommands.js";
import {
    resolveArtists,
    searchArtists,
    SpotifyQuery,
    toQueryString,
    searchAlbums,
} from "../search.js";
import { PlayerActions } from "./playerSchema.js";
import registerDebug from "debug";
import { resolveMusicDeviceEntity } from "../devices.js";
import { getUserDevices } from "../endpoints.js";
import {
    addUserDataStrings,
    getPlaylistsFromUserData,
    getUserDataCompletions,
} from "../userData.js";

const debugSpotify = registerDebug("typeagent:spotify");

export function instantiate(): AppAgent {
    return {
        initializeAgentContext: initializePlayerContext,
        updateAgentContext: updatePlayerContext,
        executeAction: executePlayerAction,
        validateWildcardMatch: validatePlayerWildcardMatch,
        resolveEntity: resolvePlayerEntity,
        getDynamicDisplay: getPlayerDynamicDisplay,
        ...getPlayerCommandInterface(),
        getActionCompletion: getPlayerActionCompletion,
        checkReadiness: checkPlayerReadiness,
    };
}

// Cheap probe for the env vars the token provider requires. Spotify
// integration is impossible without these, so we surface the missing
// configuration up front instead of letting the user discover it on the
// first action. No `setup` hook — this is a manual-config case (edit
// .env, restart the agent server); the dispatcher's setup-required
// error points the user at @config agent refresh once they've fixed it.
//
// Exported for unit tests.
export async function checkPlayerReadiness(): Promise<ReadinessReport> {
    const missing: string[] = [];
    if (!process.env.SPOTIFY_APP_CLI) missing.push("SPOTIFY_APP_CLI");
    if (!process.env.SPOTIFY_APP_CLISEC) missing.push("SPOTIFY_APP_CLISEC");
    const port = process.env.SPOTIFY_APP_PORT;
    if (!port) {
        missing.push("SPOTIFY_APP_PORT");
    } else if (parseInt(port).toString() !== port) {
        return {
            state: "setup-required",
            message: `SPOTIFY_APP_PORT has invalid port number "${port}".`,
            details:
                "Set SPOTIFY_APP_PORT to an integer in ts/.env (the redirect port your Spotify app is registered with).",
        };
    }
    if (missing.length === 0) return { state: "ready" };
    return {
        state: "setup-required",
        message: `Spotify env vars not set: ${missing.join(", ")}.`,
        details:
            "Set them in ts/.env. SPOTIFY_APP_CLI and SPOTIFY_APP_CLISEC come from the Spotify developer dashboard for your app; SPOTIFY_APP_PORT is the redirect port you registered there.",
    };
}

export type PlayerActionContext = {
    spotify: IClientContext | undefined;
};

async function initializePlayerContext() {
    return {
        spotify: undefined,
    };
}

async function executePlayerAction(
    action: TypeAgentAction<PlayerActions>,
    context: ActionContext<PlayerActionContext>,
) {
    const clientContext = context.sessionContext.agentContext.spotify;
    if (clientContext) {
        return handleCall(
            action,
            clientContext,
            context.actionIO,
            context.sessionContext.instanceStorage,
        );
    }

    return createActionResultFromError(
        "Not logged in to Spotify.  Use the command '@player spotify login' to log in.",
    );
}

async function updatePlayerContext(
    enable: boolean,
    context: SessionContext<PlayerActionContext>,
) {
    if (enable) {
        if (context.agentContext.spotify) {
            return;
        }
        try {
            const user = await enableSpotify(context, true);
            const message = `Spotify logged in as ${user}.`;
            debugSpotify(message);
            context.notify(AppAgentEvent.Info, chalk.green(message));
        } catch (e: any) {
            const message = `Spotify not logged in.  Error: ${e.message}`;
            debugSpotify(message);
            context.notify(AppAgentEvent.Error, chalk.red(message));
        }
    } else {
        disableSpotify(context);
    }
}

export async function enableSpotify(
    context: SessionContext<PlayerActionContext>,
    silent: boolean = false,
) {
    const clientContext = await getClientContext(
        context.instanceStorage,
        silent,
    );
    context.agentContext.spotify = clientContext;
    return clientContext.service.retrieveUser().username;
}

export async function disableSpotify(
    context: SessionContext<PlayerActionContext>,
    clearToken: boolean = false,
) {
    const clientContext = context.agentContext.spotify;
    if (clientContext !== undefined) {
        const timeoutId = clientContext.userData?.timeoutId;
        if (timeoutId !== undefined) {
            clearTimeout(timeoutId);
        }
        if (clearToken) {
            await clientContext.service.tokenProvider.clearRefreshToken();
        }
        context.agentContext.spotify = undefined;
    }
}

async function validatePlayerWildcardMatch(
    action: PlayerActions,
    context: SessionContext<PlayerActionContext>,
) {
    const clientContext = context.agentContext.spotify;
    if (clientContext === undefined) {
        // REVIEW: Can't validate without context, assume true
        return true;
    }
    switch (action.actionName) {
        case "playTrack":
            console.log(
                `  [Player Validation] Validating track: "${action.parameters.trackName}", artists: [${action.parameters.artists?.join(", ") || "none"}]`,
            );
            return validateTrack(
                action.parameters.trackName,
                action.parameters.artists,
                action.parameters.albumName,
                clientContext,
            );

        case "playAlbum":
            return await validateAlbum(
                action.parameters.albumName,
                action.parameters.artists,
                clientContext,
            );
        case "playArtist":
            return await validateArtist(
                action.parameters.artist,
                clientContext,
            );
        case "playGenre":
            return false;
        case "playPlaylist":
            return await validatePlayList(
                action.parameters.name,
                clientContext,
            );
        case "selectDevice": {
            const result = await resolveMusicDeviceEntity(
                clientContext,
                action.parameters.deviceName,
            );
            return result !== undefined;
        }
    }
    return true;
}

async function resolvePlayerEntity(
    type: string,
    name: string,
    context: SessionContext<PlayerActionContext>,
): Promise<ResolveEntityResult | undefined> {
    const clientContext = context.agentContext.spotify;
    if (clientContext === undefined) {
        return undefined;
    }
    switch (type) {
        case "MusicDevice":
            return resolveMusicDeviceEntity(clientContext, name);
    }
    return undefined;
}

async function validateTrack(
    trackName: string,
    artists: string[] | undefined,
    album: string | undefined,
    context: IClientContext,
) {
    if (artists === undefined && album === undefined) {
        const data = context.userData?.data;
        if (data && data.tracks) {
            if (data.nameMap === undefined) {
                addUserDataStrings(data);
            }
            // if user data has exact match return true
            if (data.nameMap!.get(trackName.toLocaleLowerCase())) {
                return true;
            }
        }
    }
    const resolvedArtists = artists
        ? await resolveArtists(artists, context)
        : [];

    if (resolvedArtists === undefined) {
        console.log(
            `    [Track Validation] ❌ Failed to resolve artists: [${artists?.join(", ")}]`,
        );
        return false;
    }

    if (artists && artists.length > 0) {
        console.log(
            `    [Track Validation] ✓ Resolved ${artists.length} artist(s): ${resolvedArtists.map((a) => a.name).join(", ")}`,
        );
    }

    const query: SpotifyQuery = {
        track: [trackName],
        artist: resolvedArtists.map((artist) => artist.name),
    };
    const queryString = toQueryString(query);
    console.log(
        `    [Track Validation] Searching Spotify for: "${queryString}"`,
    );
    const tracks = await searchTracks(queryString, context);
    if (tracks) {
        // For validation for wildcard match, only allow substring match.
        const lowerCaseTrackName = trackName.toLowerCase();
        const lowerCaseAlbumName = album?.toLowerCase();
        const found = tracks
            .getTracks()
            .some(
                (track) =>
                    track.name.toLowerCase().includes(lowerCaseTrackName) &&
                    (lowerCaseAlbumName === undefined ||
                        track.album.name
                            .toLowerCase()
                            .includes(lowerCaseAlbumName)),
            );
        if (found) {
            console.log(`    [Track Validation] ✓ Track found in Spotify`);
        } else {
            console.log(
                `    [Track Validation] ❌ Track not found in Spotify results`,
            );
        }
        return found;
    }
    console.log(`    [Track Validation] ❌ Spotify search returned no results`);
    return false;
}

async function validateAlbum(
    albumName: string,
    artists: string[] | undefined,
    context: IClientContext,
) {
    const resolvedArtists = artists
        ? await resolveArtists(artists, context)
        : [];

    if (resolvedArtists === undefined) {
        return false;
    }
    const albums = await searchAlbums(albumName, undefined, context);
    if (albums === undefined) {
        return false;
    }

    const lowerCaseAlbumName = albumName.toLowerCase();
    // For validation for wildcard match, only allow substring match.
    const filteredAlbums = albums.filter((album) =>
        album.name.toLowerCase().includes(lowerCaseAlbumName),
    );

    if (filteredAlbums.length === 0) {
        return false;
    }

    if (artists === undefined) {
        return true;
    }

    return resolvedArtists.every((resolvedArtist) =>
        filteredAlbums.some((album) =>
            album.artists.some((artists) => artists.id === resolvedArtist.id),
        ),
    );
}

async function validateArtist(artistName: string, context: IClientContext) {
    // if user data has exact match return true
    const userData = context.userData;
    if (userData && userData.data) {
        const artists = userData.data.artists;
        if (artists) {
            if (!userData.data.nameMap) {
                addUserDataStrings(userData.data);
            }
            if (userData.data.nameMap!.get(artistName.toLocaleLowerCase())) {
                return true;
            }
        }
    }

    const data = await searchArtists(artistName, context);

    if (data && data.artists && data.artists.items.length > 0) {
        // For validation for wildcard match, only allow substring match.
        const lowerCaseArtistName = artistName.toLowerCase();
        return data.artists.items.some((artist) =>
            artist.name.toLowerCase().includes(lowerCaseArtistName),
        );
    }
    return false;
}

async function validatePlayList(
    playlistName: string,
    context: IClientContext,
): Promise<boolean> {
    // Check user data for an exact match first
    const userData = context.userData;
    if (userData && userData.data) {
        if (userData.data.playlists === undefined) {
            await getPlaylistsFromUserData(context.service, userData.data);
        }
        if (userData.data.playlists) {
            const lowerCaseName = playlistName.toLowerCase();
            if (
                userData.data.playlists.some((pl) =>
                    pl.name.toLowerCase().includes(lowerCaseName),
                )
            ) {
                return true;
            }
        }
    }

    // Fall back to searching Spotify
    const playlists = await searchForPlaylists(playlistName, context);
    if (playlists && playlists.length > 0) {
        const lowerCaseName = playlistName.toLowerCase();
        return playlists.some((pl) =>
            pl.name.toLowerCase().includes(lowerCaseName),
        );
    }
    return false;
}

async function getPlayerDynamicDisplay(
    type: DisplayType,
    displayId: string,
    context: SessionContext<PlayerActionContext>,
): Promise<DynamicDisplay> {
    if (context.agentContext.spotify === undefined) {
        return {
            content: "Spotify integration is not enabled.",
            nextRefreshMs: -1,
        };
    }
    if (displayId === "status") {
        const status = await htmlStatus(context.agentContext.spotify);
        return {
            content:
                type === "html" ? status.displayContent : status.historyText!,

            nextRefreshMs: 1000,
        };
    }
    throw new Error(`Invalid displayId ${displayId}`);
}

async function getPlayerActionCompletion(
    context: SessionContext<PlayerActionContext>,
    action: AppAction,
    propertyName: string,
    _entityType?: string,
): Promise<string[]> {
    const result: string[] = [];
    const clientContext = context.agentContext.spotify;
    if (clientContext === undefined) {
        return result;
    }

    const userData = clientContext.userData;
    if (userData === undefined) {
        return result;
    }

    let track = false;
    let artist = false;
    let album = false;
    let playlist = false;
    switch (action.actionName) {
        case "playTrack":
            if (propertyName === "parameters.trackName") {
                track = true;
            } else if (
                propertyName === "parameters.artists" ||
                propertyName.startsWith("parameters.artists.")
            ) {
                artist = true;
            } else if (propertyName === "parameters.albumName") {
                album = true;
            }
            break;
        case "playAlbum":
            if (propertyName === "parameters.albumName") {
                album = true;
            } else if (
                propertyName === "parameters.artists" ||
                propertyName.startsWith("parameters.artists.")
            ) {
                artist = true;
            }
            break;
        case "playArtist":
            if (propertyName === "parameters.artist") {
                artist = true;
            }
            break;
        case "playGenre":
            break;
        case "playPlaylist":
            if (propertyName === "parameters.name") {
                playlist = true;
            }
            break;
        case "getPlaylist":
        case "deletePlaylist":
        case "addCurrentTrackToPlaylist":
        case "addToPlaylistFromCurrentTrackList":
        case "addSongsToPlaylist":
            if (propertyName === "parameters.name") {
                if (userData.data.playlists === undefined) {
                    await getPlaylistsFromUserData(
                        clientContext.service,
                        userData.data,
                    );
                }
                if (userData.data.playlists) {
                    const names = userData.data.playlists
                        .map((pl) => pl.name)
                        .sort();
                    result.push(...names);
                }
            }
            return result;
        case "selectDevice":
            if (propertyName === "parameters.deviceName") {
                const devices = await getUserDevices(clientContext.service);
                if (devices !== undefined) {
                    result.push(
                        ...devices.devices
                            .filter((device) => device.id !== null)
                            .map((device) => device.name),
                    );
                }
            }
            return result;
    }

    result.push(
        ...getUserDataCompletions(
            userData.data,
            track,
            artist,
            album,
            playlist,
        ),
    );
    return result;
}
