import { Logger } from '@foxxmd/logging';
import { Dayjs, ManipulateType } from "dayjs";
import { Request, Response } from "express";
import { type NextFunction, type ParamsDictionary, type Query } from "express-serve-static-core";
import { FixedSizeList } from 'fixed-size-list';
import { ErrorLike, isPlayObject, PlayMetaLifecycleless, PlayObject, PlayObjectLifecycleless, UnixTimestamp } from "../../../core/Atomic.js";
import TupleMap from "../TupleMap.js";
import { MusicBrainzApi } from 'musicbrainz-api';
import { SourceType } from './config/source/sources.js';
import { ClientType, clientTypes } from './config/client/clients.js';
import assert, { AssertionError } from 'assert';

export const lowGranularitySources: SourceType[] = ['subsonic', 'ytmusic'];

export const isClientType = (data: string): data is ClientType => {
    return clientTypes.includes(data as ClientType);
}

export interface ComponentIdentifier {
    type: SourceType | ClientType
    name: string
}

export interface SourceIdentifier extends ComponentIdentifier {
    type: SourceType
}

export interface ClientIdentifier extends ComponentIdentifier {
    type: ClientType
}

export type InitState = 0 | 1 | 2;
export const NOT_INITIALIZED: InitState = 0;
export const INITIALIZING: InitState = 1;
export const INITIALIZED: InitState = 2;
export const initStates: InitState[] = [NOT_INITIALIZED, INITIALIZING, INITIALIZED];

export type ReadyState = 0 | 1 | 2;
export const NOT_READY: ReadyState = 0;
export const GETTING_READY: ReadyState = 1;
export const READY: ReadyState = 2;
export const readyStates: ReadyState[] = [NOT_READY, GETTING_READY, READY];

export interface InternalConfig {
    localUrl: URL
    configDir: string
    version: string

    logger: Logger
}

export type InternalConfigOptional = Omit<InternalConfig, 'logger'>

export type ReportedPlayerStatus = 'playing' | 'stopped' | 'paused' | 'unknown';
export const REPORTED_PLAYER_STATUSES = {
    playing: 'playing' as ReportedPlayerStatus,
    stopped: 'stopped' as ReportedPlayerStatus,
    paused: 'paused' as ReportedPlayerStatus,
    unknown: 'unknown' as ReportedPlayerStatus
}

export type CalculatedPlayerStatus = ReportedPlayerStatus | 'stale' | 'orphaned';
export const CALCULATED_PLAYER_STATUSES = {
    ...REPORTED_PLAYER_STATUSES,
    stale: 'stale' as CalculatedPlayerStatus,
    orphaned: 'orphaned' as CalculatedPlayerStatus,
}

export type ConfigureAsSource = 'source';
export type ConfigureAsClient = 'client';
export type ConfigureAs = ConfigureAsSource | ConfigureAsClient;

export const CONFIGURE_AS = {
    source: 'source',
    client: 'client'
} as const satisfies Record<string, ConfigureAs>

export interface ConfigMeta {
    source: string
    mode?: string
    configureAs: ConfigureAs
}

export type SourceData = (PlayObject | PlayerStateData);

export interface PlayerStateData extends PlayerStateDataMaybePlay {
    play: PlayObject
}

export interface PlayerStateDataMaybePlay {
    platformId: PlayPlatformId
    /** The ID/Key for individual sessions on a device/platform */
    sessionId?: string
    play?: PlayObject
    status?: ReportedPlayerStatus
    position?: number
    stateUpdatedAt?: Dayjs
    playUpdatedAt?: Dayjs
}

export const asPlayerStateData = (obj: object): obj is PlayerStateData => asPlayerStateDataMaybePlay(obj) && 'play' in obj && isPlayObject(obj.play)

export const asPlayerStateDataMaybePlay = (obj: object): obj is PlayerStateDataMaybePlay => 'platformId' in obj

export interface FormatPlayObjectOptions {
    newFromSource?: boolean
    parsedFrom?: string

    [key: string]: any
}

export interface ProgressAwarePlayObject extends PlayObjectLifecycleless {
    meta: PlayMetaLifecycleless & {
        initialTrackProgressPosition?: number
    }
}
export type DeviceId = string;
export type PlayUserId = string;
export type PlayPlatformId = [DeviceId, PlayUserId];
export type PlayPlatformIdStr = string;

export type GroupedPlays = TupleMap<DeviceId,PlayUserId,ProgressAwarePlayObject[]>;

export type GroupedFixedPlays = TupleMap<DeviceId,PlayUserId,FixedSizeList<ProgressAwarePlayObject>>;

export const NO_DEVICE = 'NoDevice';
export const NO_USER = 'SingleUser';

export const SINGLE_USER_PLATFORM_ID: PlayPlatformId = [NO_DEVICE, NO_USER];
export const SINGLE_USER_PLATFORM_ID_STR = `${NO_DEVICE}-${NO_USER}`;

export interface ScrobbledPlayObject {
    play: PlayObject
    scrobble: PlayObjectLifecycleless
}


export interface RemoteIdentityParts {
    host: string,
    proxy: string | undefined,
    agent: string | undefined
}

/**
 * https://www.last.fm/api/scrobbling (When is a scrobble a scrobble?)
 * https://github.com/krateng/maloja/blob/master/API.md#scrobbling-guideline
 * */
export const DEFAULT_SCROBBLE_DURATION_THRESHOLD: number = 240;
/**
 * https://www.last.fm/api/scrobbling (When is a scrobble a scrobble?)
 * https://github.com/krateng/maloja/blob/master/API.md#scrobbling-guideline
 * */
export const DEFAULT_SCROBBLE_PERCENT_THRESHOLD: number = 50;

export const DEFAULT_POLLING_INTERVAL: number = 10;
export const DEFAULT_POLLING_MAX_INTERVAL = 30;

export const DEFAULT_RETRY_MULTIPLIER: number = 1.5;

export const DEFAULT_CLOSE_POSITION_ABSOLUTE = 12;
export const DEFAULT_CLOSE_POSITION_PERCENT = 0.15;
export const DEFAULT_DURATION_REPEAT_ABSOLUTE = 120;
export const DEFAULT_DURATION_REPEAT_PERCENT = 0.50;
export interface ScrobbleThresholdResult {
    passes: boolean
    duration: {
        passes?: boolean
        value?: number
        threshold?: number
    }
    percent: {
        passes?: boolean
        value?: number
        threshold?: number
    }

}

export interface RegExResult {
    match: string,
    groups: string[],
    index: number
    named: NamedGroup
}

export interface NamedGroup {
    [name: string]: any
}

export type ExpressRequest = Request<ParamsDictionary, any, any, Query, Record<string, any>>;
export type ExpressResponse = Response<any, Record<string, any>>;
export type ExpressHandler = (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => Promise<void | Response<any, Record<string, any>>>

export const DELIMITERS = [',','&','/','\\'];
export const DELIMITERS_NO_AMP = [',','/','\\'];
export const DELIMETERS_REGEX: RegExp = new RegExp(/[,&\/\\]/);

export const ARTIST_WEIGHT = 0.3;
export const TITLE_WEIGHT = 0.4;
export const TIME_WEIGHT = 0.5;
export const REFERENCE_WEIGHT = 0.5;
export const DUP_SCORE_THRESHOLD = 1;

// https://stackoverflow.com/a/70887388/1469797
export type ArbitraryObject = { [key: string]: unknown; };
export function isArbitraryObject(potentialObject: unknown): potentialObject is ArbitraryObject {
    return typeof potentialObject === "object" && potentialObject !== null;
}

export interface Authenticatable {
    requiresAuth: boolean
    requiresAuthInteraction: boolean
    authed: boolean
    authFailure?: boolean
    testAuth: () => Promise<any>
}

export interface ScrobbleRangeFetchable {
    getScrobblesForTimeRange: TimeRangeListensFetcher
}

export interface MdnsDeviceInfo {
    name: string
    type: string
    addresses: string[]
}

export type AbstractApiOptions = Record<any, any> & { logger: Logger }

export type keyOmit<T, U extends keyof any> = T & { [P in U]?: never }

export type WithRequiredProperty<Type, Key extends keyof Type> = Type & {
  [Property in Key]-?: Type[Property];
};
export type CacheProvider = 'memory' | 'valkey' | 'file' | false;
export interface CacheConfig<T extends CacheProvider = CacheProvider> {
    provider: T;
    connection?: string;
    [key: string]: any
}
export interface CacheMemoryConfig extends CacheConfig<'memory'> {
    lruSize?: number
    ttl?: number
}
export interface CacheValkeyConfig extends CacheConfig<'valkey'> {
}
export interface CacheFileConfig extends CacheConfig<'file'> {
}
export interface CacheNoopConfig extends CacheConfig<false> {
}

export type CacheConfigType = CacheMemoryConfig | CacheValkeyConfig | CacheFileConfig | CacheNoopConfig;

export type CacheMetadataProvider = CacheProvider;//Exclude<CacheProvider, 'file'>;
export type CacheMetadataConfig = CacheConfigType;
export const asCacheProvider = (val: boolean | string): val is CacheProvider => {
    if(typeof val === 'string') {
        return ['memory', 'valkey', 'file'].includes(val);
    }
    return val === false;
}
export const asCacheEphemeralConfig = (val: Record<string, any>): val is (CacheMemoryConfig | CacheNoopConfig) => {
    return val.provider === false || val.provider === 'memory';
}
export const asCacheConnectableConfig = (val: Record<string, any>): val is (CacheValkeyConfig | CacheFileConfig) => {
    if(!['valkey', 'file'].includes(val.provider)) {
        return false;
    }
    return typeof val.connection === 'string';
}
export const asCacheConfig = (val: Record<string, any>): val is CacheConfigType => {
    if(!asCacheProvider(val.provider)) {
        assert(asCacheProvider(val.provider), `Cache provider must be one of: 'memory', 'valkey', 'file', or false`);
    }
    if(asCacheConnectableConfig(val) || asCacheEphemeralConfig(val)) {
        return true;
    }
    throw new Error(`${val.provider} must have a connection property that is a string`);
}
export const asCacheMetadataProvider = (val: any): val is CacheScrobbleProvider => asCacheProvider(val);
export type CacheScrobbleProvider = CacheProvider;
export type CacheScrobbleConfig = CacheConfigType;
export const asCacheScrobbleProvider = (val: any): val is CacheScrobbleProvider => asCacheProvider(val);

export type CacheAuthProvider = CacheProvider;
export type CacheAuthConfig = CacheConfigType;
export const asCacheAuthProvider = (val: any): val is CacheAuthProvider => asCacheProvider(val);
export interface CacheConfigOptions {
    metadata?: CacheMetadataConfig;
    scrobble?: CacheScrobbleConfig;
    auth?: CacheAuthConfig;
    /** Number of regex functions to cache (LRU)
     * 
     * @default 200
     */
    regex?: number
}

export interface CacheConfigUser {
    auth?: {
        provider: 'valkey' | 'file',
        [key: string]: any
    };
    valkey?: string
    /** Number of regex functions to cache (LRU)
     * 
     * @default 200
     */
    regex?: number
    // to allow deprecated scrobble config without having it show up in schema docs
    [key: string]: any
}

export interface MusicbrainzApiConfigData {
    url?: string
    contact: string,
    apiKey?: string
    requestTimeout?: number
}

export const MUSICBRAINZ_URL = 'https://musicbrainz.org';
export const MBID_VARIOUS_ARTISTS = "89ad4ac3-39f7-470e-963a-56509c546377";

export type MusicBrainzSingletonMap = Map<string,MusicBrainzApi>;



/* https://websocket.org/reference/close-codes/ */
export const WEBSOCKET_CLOSE_CODE_REASONS = {
    1000: 'Closed gracefully',
    1001: 'Going Away',
    1002: 'Protocol Error',
    1003: 'Unsupported Data',
    1005: 'No Status Received',
    1006: 'Abnormal Closure (Connection lost unexpectedly)',
    1008: 'Policy Violation',
    1009: 'Message Too Big',
    1011: 'Server-side exception',
    1012: 'Service Restart',
    1013: 'Try Again Later',
    1014: 'Bad Gateway',
    1015: 'TLD handshake fail'
}

export const WEBSOCKET_CLOSE_CODE = {
    AbnormalClosure: 1006,
    InternalError: 1011
}

export const WEBSOCKET_CLOSE_CODES_RETRY = [1006,1011,1001];export interface PaginatedLimit {
    /** per page max number of results to return */
    limit?: number
}

export interface PaginatedTimeRangeOptions {
    /** Unix timestamp */
    from: UnixTimestamp
    /** Unix timestamp */
    to: UnixTimestamp
    /** maximum number of results to fetch before returning early */
    fetchMax?: number
}

export type PaginatedTimeRangeCommonOptions = Partial<PaginatedTimeRangeOptions> & PaginatedLimit;

export type CursorType = number | string;

export interface PaginatedListensOptions<T extends CursorType> extends PaginatedLimit {
    cursor: T
}

export interface PagelessListensTimeRangeOptions extends PaginatedTimeRangeCommonOptions {
}

export interface PaginatedListensTimeRangeOptions<T extends CursorType = CursorType> extends Partial<PaginatedTimeRangeOptions>, PaginatedListensOptions<T> {
}

export interface PaginatedResults<T extends CursorType = CursorType> {
    total?: number
    more?: boolean
    cursorNext?: T
    order?: 'desc' | 'asc'
}

export interface PaginatedListens {
    getPaginatedListens(params: PaginatedListensOptions<CursorType>): Promise<{data: PlayObject[], meta: PaginatedListensOptions<CursorType> & PaginatedResults<CursorType>}>
}

export const hasPaginagedListens = (obj: Object): obj is PaginatedListens => {
    return 'getPaginatedListens' in obj;
}

export interface PaginatedTimeRangeListensResult<T extends CursorType> {
    data: PlayObject[];
    meta: PaginatedListensTimeRangeOptions<T> & PaginatedResults<T>;
}
export interface PaginatedTimeRangeListens<T extends CursorType = CursorType> {
    getPaginatedTimeRangeListens(params: PaginatedListensTimeRangeOptions<T>): Promise<PaginatedTimeRangeListensResult<T>>
    getPaginatedUnitOfTime(): ManipulateType;
}

export const hasPaginatedTimeRangeListens = (obj: Object): obj is PaginatedTimeRangeListens => {
    return 'getPaginatedTimeRangeListens' in obj;
}

export interface PagelessTimeRangeListensResult {
    data: PlayObject[]
    meta: PagelessListensTimeRangeOptions & PaginatedResults
}
export interface PagelessTimeRangeListens {
    getPagelessTimeRangeListens(params: PagelessListensTimeRangeOptions): Promise<PagelessTimeRangeListensResult>
    getPaginatedUnitOfTime(): ManipulateType;
}

export const hasPagelessTimeRangeListens = (obj: Object): obj is PagelessTimeRangeListens => {
    return 'getPagelessTimeRangeListens' in obj;
}

export type PaginatedTimeRangeSource = PaginatedTimeRangeListens | PagelessTimeRangeListens;
export type PaginatedSource = PaginatedListens | PaginatedTimeRangeSource;

export type TimeRangeListensFetcher<T extends CursorType = CursorType> = (opts: PaginatedTimeRangeCommonOptions | PaginatedListensTimeRangeOptions<T>) => Promise<PlayObject[]>

export interface ScrobbleRangeResult {
    plays: PlayObject[]
    fetchedAt: Dayjs
}

export const REFRESH_STALE_DEFAULT = 60;

/**
 * A duration of time
 * 
 * May be either:
 * 
 * * a `number` of seconds
 * * a `string` containing a number and a unit of time compatible with dayjs
 * 
 * @example [60, 3600, "1 hour", "4 days"]
 */
export type DurationValue = number | string;

export type MigrationStatus = {
    backupRequired: boolean, 
    pending: string[],
    reason?: string,
    log?: string,
    error?: ErrorLike
};