import type { DailyTaskData, QuestMessageData, RewardTrackData, RewardTrackPrizeData } from '@octane/renderer';
import { GetConfigurationValue } from '../octane';
import { LocalizeText, localizeWithFallback } from '../utils';

/** The official quest engine texts and image rules (HabboQuestEngine / QuestsList / QuestCompleted). */

export const QUEST_TIMER_IMAGE = 'quest_timer_questionmark';

/** The prompt-animated quests of the official client (their image carries an `_a` suffix). */
export const QUESTS_WITH_PROMPTS = ['MOVEITEM', 'ENTEROTHERSROOM', 'CHANGEFIGURE', 'FINDLIFEGUARDTOWER', 'SCRATCHAPET'];

/** `${image.library.questing.url}` with the generic image library as fallback. */
export const getQuestingImageBaseUrl = (): string => {
    const configured = GetConfigurationValue<string>('image.library.questing.url', '');

    if (configured && configured.length) return configured;

    return `${GetConfigurationValue<string>('image.library.url', '')}questing/`;
};

export const getQuestingImageUrl = (name: string): string => `${getQuestingImageBaseUrl()}${name}.png`;

/** `<campaign>_<code><imageVersion>[_a]`, lower-cased, or the hourglass while the quest waits. */
export const getQuestImageName = (campaignCode: string, localizationCode: string, imageVersion: string, waiting: boolean = false): string => {
    if (waiting) return QUEST_TIMER_IMAGE;

    const prompt = QUESTS_WITH_PROMPTS.includes((localizationCode || '').toUpperCase()) ? '_a' : '';

    return `${campaignCode}_${localizationCode}${imageVersion || ''}${prompt}`.toLowerCase();
};

export const getQuestImageUrl = (quest: QuestMessageData, waiting: boolean = quest.waitPeriodSeconds > 0): string =>
    getQuestingImageUrl(getQuestImageName(quest.campaignCode, quest.localizationCode, quest.imageVersion, waiting));

export const getCampaignImageUrl = (campaignCode: string): string => getQuestingImageUrl(campaignCode);

export const getCampaignLocalizationKey = (campaignCode: string): string => `quests.${campaignCode}`;

export const getCampaignName = (campaignCode: string): string => localizeWithFallback(`${getCampaignLocalizationKey(campaignCode)}.name`, campaignCode);

export const getQuestLocalizationKey = (quest: QuestMessageData): string => `${getCampaignLocalizationKey(quest.campaignCode)}.${quest.localizationCode}`;

export const getQuestName = (quest: QuestMessageData): string => localizeWithFallback(`${getQuestLocalizationKey(quest)}.name`, quest.localizationCode);

export const getQuestDescription = (quest: QuestMessageData): string => localizeWithFallback(`${getQuestLocalizationKey(quest)}.desc`, '');

export const getQuestHint = (quest: QuestMessageData): string => localizeWithFallback(`${getQuestLocalizationKey(quest)}.hint`, '');

export const getQuestCompletedText = (quest: QuestMessageData): string =>
    localizeWithFallback(`${getQuestLocalizationKey(quest)}.completed`, getQuestDescription(quest));

/** The tracker bar: ceil(100 * completed / total), clamped. */
export const getQuestProgressPercent = (completedSteps: number, totalSteps: number): number => {
    if (!totalSteps || totalSteps <= 0) return 0;

    return Math.max(0, Math.min(100, Math.ceil((100 * completedSteps) / totalSteps)));
};

export type CampaignCounterStyle = 'red' | 'blue' | 'green';

/** quest_counterbkg_disabled / _active / _completed of the official campaign block. */
export const getCampaignCounterStyle = (completedQuests: number, completedCampaign: boolean): CampaignCounterStyle => {
    if (completedCampaign) return 'green';

    return completedQuests > 0 ? 'blue' : 'red';
};

/** The official list hides the reward when the type is unknown or the amount is empty. */
export const isQuestRewardVisible = (activityPointType: number, amount: number): boolean => activityPointType >= -1 && amount > 0;

/** activityPointType -1 is credits, every other value is a points type (0 duckets, 5 diamonds). */
export const getActivityPointName = (activityPointType: number): string => {
    if (activityPointType === -1) return localizeWithFallback('quests.currency.credits', 'Credits');

    if (activityPointType === 0) return localizeWithFallback('quests.currency.duckets', 'Duckets');

    if (activityPointType === 5) return localizeWithFallback('quests.currency.diamonds', 'Diamonds');

    return localizeWithFallback(`activitypoint.name.${activityPointType}`, 'Points');
};

/** The currency icon type of `LayoutCurrencyIcon`: credits use -1 in the client sprite set. */
export const getActivityPointIconType = (activityPointType: number): number => activityPointType;

// ---------------------------------------------------------------- next quest timer

/** NextQuestTimer.REFRESH_PERIOD_IN_MSECS: the countdown is redrawn once a second. */
export const NEXT_QUEST_TIMER_REFRESH_MS = 1000;

const FRIENDLY_TIME_UNITS: { name: string; seconds: number; fallback: string }[] = [
    { name: 'years', seconds: 365 * 86400, fallback: '%amount% years' },
    { name: 'months', seconds: 30 * 86400, fallback: '%amount% months' },
    { name: 'days', seconds: 86400, fallback: '%amount% days' },
    { name: 'hours', seconds: 3600, fallback: '%amount% hours' },
    { name: 'minutes', seconds: 60, fallback: '%amount% min' }
];

/**
 * FriendlyTime.getFriendlyTime of the official client: the largest unit the time exceeds three
 * times, rounded, as `friendlytime.<unit>` with `%amount%`.
 */
export const getFriendlyTimeParts = (seconds: number, threshold: number = 3): { key: string; amount: number; fallback: string } => {
    for (const unit of FRIENDLY_TIME_UNITS) {
        if (seconds > threshold * unit.seconds) return { key: `friendlytime.${unit.name}`, amount: Math.round(seconds / unit.seconds), fallback: unit.fallback };
    }

    return { key: 'friendlytime.seconds', amount: Math.round(seconds), fallback: '%amount% sec' };
};

export const formatQuestWaitTime = (seconds: number): string => {
    const { key, amount, fallback } = getFriendlyTimeParts(Math.max(0, seconds));
    const value = String(amount);

    return localizeWithFallback(key, fallback.replace('%amount%', value), ['amount', 'AMOUNT'], [value, value]);
};

/**
 * The wait left on a quest. `QuestMessageData.waitPeriodSeconds` counts itself down from the moment
 * the packet arrived; `clock` is the caller's tick so a memoizing render re-reads it every second.
 */
export const getQuestWaitSecondsLeft = (quest: QuestMessageData, clock: number): number => {
    if (!quest || !Number.isFinite(clock)) return 0;

    return Math.max(0, quest.waitPeriodSeconds || 0);
};

/** `quests.<campaign>.delayedmsg`, the text of the expanded timer. */
export const getQuestDelayedMessageKey = (quest: QuestMessageData): string => `${getCampaignLocalizationKey(quest.campaignCode)}.delayedmsg`;

export const getNextQuestTimerCaption = (expanded: boolean, time: string): string =>
    expanded
        ? localizeWithFallback('quests.nextquesttimer.caption.expanded', 'Time until next Quest')
        : localizeWithFallback('quests.nextquesttimer.caption.contracted', `Next Quest: ${time}`, ['time'], [time]);

export const getNextQuestTimerMessage = (quest: QuestMessageData, time: string): string =>
    localizeWithFallback(getQuestDelayedMessageKey(quest), `Your next Quest is available in <b>${time}</b>`, ['time'], [time]);

// ---------------------------------------------------------------- daily tasks

export type DailyTaskStyle = 'orange' | 'green' | 'yellow';

/** Orange while in progress, green once claimable or claimed; bonus rows stay yellow. */
export const getDailyTaskStyle = (status: number, isBonus: boolean): DailyTaskStyle => {
    if (isBonus) return 'yellow';

    return status === 0 ? 'orange' : 'green';
};

export const getDailyTaskProgressPercent = (repeats: number, requiredRepeats: number): number => {
    if (!requiredRepeats || requiredRepeats <= 0) return 100;

    return Math.max(0, Math.min(100, Math.floor((repeats / requiredRepeats) * 100)));
};

export const getDailyTaskImageUrl = (task: DailyTaskData): string => {
    const configured = GetConfigurationValue<string>('image.library.dailytasks.url', '');
    const base = configured && configured.length ? configured : `${GetConfigurationValue<string>('image.library.url', '')}dailytasks/`;

    return `${base}${task.taskCode}${task.imageVersion || ''}.png`;
};

/** Bonus tasks go last, the official controller order. */
export const sortDailyTasks = (tasks: DailyTaskData[]): DailyTaskData[] => [...tasks.filter((task) => !task.isBonus), ...tasks.filter((task) => task.isBonus)];

export const getDailyTasksWindowCaption = (maxSecondsLeft: number): string => {
    const title = localizeWithFallback('dailytasks.title', 'Daily rewards');

    if (maxSecondsLeft <= 0) return title;

    const refresh = localizeWithFallback('dailytasks.refreshes', 'Refresh in %time%', ['time'], [formatFriendlySeconds(maxSecondsLeft)]);

    return `${title} - ${refresh}`;
};

/** "2h 5m" style countdowns; the official client uses FriendlyTime, this keeps it self-contained. */
export const formatFriendlySeconds = (seconds: number): string => {
    const total = Math.max(0, Math.floor(seconds));
    const days = Math.floor(total / 86400);
    const hours = Math.floor((total % 86400) / 3600);
    const minutes = Math.floor((total % 3600) / 60);
    const secs = total % 60;

    if (days > 0) return `${days}d ${hours}h`;

    if (hours > 0) return `${hours}h ${minutes}m`;

    if (minutes > 0) return `${minutes}m ${secs}s`;

    return `${secs}s`;
};

// ---------------------------------------------------------------- reward track

export type RewardTrackPrizeState = 'claimed' | 'premium_locked' | 'not_enough_points' | 'claimable';

/** The four tooltip states of RewardTrackPrizeView.refreshState(). */
export const getRewardTrackPrizeState = (prize: RewardTrackPrizeData, track: RewardTrackData): RewardTrackPrizeState => {
    if (prize.claimed) return 'claimed';

    if (prize.isPremiumLocked(track)) return 'premium_locked';

    if (!prize.hasEnoughPoints(track)) return 'not_enough_points';

    return 'claimable';
};

export const getRewardTrackPrizeTooltip = (state: RewardTrackPrizeState): string => {
    switch (state) {
        case 'claimed':
            return localizeWithFallback('reward_track.rewards.reward_tooltip.claimed', 'Already claimed');
        case 'premium_locked':
            return localizeWithFallback('reward_track.rewards.reward_tooltip.premium', 'Premium reward - upgrade to claim it');
        case 'not_enough_points':
            return localizeWithFallback('reward_track.rewards.reward_tooltip.not_enough_points', 'Collect more points to claim this reward');
        default:
            return localizeWithFallback('reward_track.rewards.reward_tooltip.claim', 'Click to claim');
    }
};

export interface RewardTrackTheme {
    dark: string;
    medium: string;
    light: string;
    active: string;
}

/** RewardTrackTheme.resolve(): five recolour themes, blue is the fallback. */
export const REWARD_TRACK_THEMES: Record<string, RewardTrackTheme> = {
    blue: { dark: '#3576B9', medium: '#CFE2F9', light: '#DDEBF9', active: '#BDD6EF' },
    orange: { dark: '#C97918', medium: '#FFDFB2', light: '#FFEFD6', active: '#FFCF91' },
    forest_green: { dark: '#3F8A45', medium: '#CDEACB', light: '#E1F3DF', active: '#B8DFB6' },
    red: { dark: '#B84B4B', medium: '#F1CCCC', light: '#F8DDDD', active: '#E7B8B8' },
    cyan: { dark: '#1F9EB3', medium: '#C7EFF5', light: '#DCF7FB', active: '#B5E9F1' }
};

export const resolveRewardTrackTheme = (theme: string): RewardTrackTheme => REWARD_TRACK_THEMES[theme] ?? REWARD_TRACK_THEMES.blue;

/**
 * The window the hint button opens for a task. The official client reads the link from the
 * `reward_track.<track>.task.<task>.hint.internal_link` property and hides the button when it is
 * empty; `configuredLink` is that property. Without one, the official actions whose hint has a
 * button text (`task.<id>.hint.button_text`) open their window; the others show no button.
 */
export const getRewardTrackTaskHintLink = (actionType: string, configuredLink: string = null): { fallbackText: string; link: string } | null => {
    if (configuredLink) return { fallbackText: '', link: configuredLink };

    switch ((actionType || '').toLowerCase()) {
        case 'enter_other_users_room':
        case 'chat_with_someone':
            return { fallbackText: 'Open the Navigator', link: 'navigator/show' };
        case 'create_room':
            return { fallbackText: 'Open the Navigator', link: 'navigator/create' };
        case 'place_item':
            return { fallbackText: 'Open the inventory', link: 'inventory/show' };
        case 'wear_badge':
            return { fallbackText: 'Open the inventory', link: 'inventory/show/badges' };
        case 'buy_from_catalogue':
            return { fallbackText: 'Open the Catalogue', link: 'catalog/show' };
        case 'change_figure':
            return { fallbackText: 'Open the Look Editor', link: 'avatar-editor/show' };
        case 'request_friend':
            return { fallbackText: 'Find friends', link: 'friends/show' };
        case 'publish_picture':
            return { fallbackText: 'Open the Camera', link: 'camera/show' };
        default:
            return null;
    }
};

export const getRewardTrackText = (trackId: string, suffix: string, fallback: string): string =>
    localizeWithFallback(`reward_track.${trackId}.${suffix}`, fallback);

export const getRewardTrackTaskText = (trackId: string, taskId: string, suffix: string, fallback: string): string =>
    localizeWithFallback(`reward_track.${trackId}.task.${taskId}.${suffix}`, fallback);

export type RewardTrackTaskFilter = 'all' | 'in_progress' | 'completed';

export const filterRewardTrackTasks = <T extends { hasProgress: boolean; isComplete: boolean }>(tasks: T[], filter: RewardTrackTaskFilter): T[] => {
    switch (filter) {
        case 'in_progress':
            return tasks.filter((task) => task.hasProgress && !task.isComplete);
        case 'completed':
            return tasks.filter((task) => task.isComplete);
        default:
            return tasks;
    }
};

/** Splits a single tier into pages of `perPage` prizes, ordered by required points. */
export const paginatePrizes = <T extends { requiredPoints: number }>(prizes: T[], perPage: number): T[][] => {
    const ordered = [...prizes].sort((a, b) => a.requiredPoints - b.requiredPoints);
    const pages: T[][] = [];

    for (let i = 0; i < ordered.length; i += Math.max(1, perPage)) pages.push(ordered.slice(i, i + Math.max(1, perPage)));

    return pages.length ? pages : [[]];
};

/** The premium confirmation shows the boost as a percentage: round((boost - 1) * 100). */
/**
 * One page per span of points milestones, so the free and the premium row stay
 * column-aligned: the Nth column of both rows is the same milestone, and a
 * milestone that only one tier rewards leaves the other row's column empty
 * instead of shifting every prize after it.
 */
export const paginatePrizeTiers = <T extends { premium: boolean; requiredPoints: number }>(
    prizes: T[],
    milestonesPerPage: number
): { free: T[][]; milestones: number[][]; premium: T[][] } => {
    const perPage = Math.max(1, milestonesPerPage);
    const milestones = [...new Set(prizes.map((prize) => prize.requiredPoints))].sort((a, b) => a - b);
    const free: T[][] = [];
    const premium: T[][] = [];
    const pageMilestones: number[][] = [];

    for (let index = 0; index < milestones.length; index += perPage) {
        const page = milestones.slice(index, index + perPage);
        const span = new Set(page);

        pageMilestones.push(page);
        const ofPage = (tier: boolean) => prizes.filter((prize) => prize.premium === tier && span.has(prize.requiredPoints)).sort((a, b) => a.requiredPoints - b.requiredPoints);

        free.push(ofPage(false));
        premium.push(ofPage(true));
    }

    return free.length ? { free, milestones: pageMilestones, premium } : { free: [[]], milestones: [[]], premium: [[]] };
};

export const getPremiumBoostPercent = (taskPointsBoost: number): number => Math.round((taskPointsBoost - 1) * 100);

/** The official client localizes non-zero result codes as `<prefix><code>`. */
export const getRewardTrackResultText = (prefix: string, resultCode: number, fallback: string): string => {
    if (resultCode === 0) return localizeWithFallback(`${prefix}success`, fallback);

    return localizeWithFallback(`${prefix}fail.${resultCode}`, LocalizeText(`${prefix}fail.${resultCode}`) || fallback);
};

/** How a daily task reward is drawn (AIR DailyTaskRewardView over ProductIconWidget), or null for no icon. */
export type DailyTaskRewardIcon =
    | { kind: 'currency'; type: number }
    | { kind: 'badge'; code: string }
    | { kind: 'furni'; productType: 's' | 'i'; classId: number }
    | null;

const LEGACY_DAILY_TASK_CURRENCIES: Record<string, number> = { credits: -1, duckets: 0, diamonds: 5 };

/**
 * AIR ProductIconWidget: product type 8 is a currency (the item type is its activity point type), 4 a badge (the
 * item type is the code), 1 and 11 a floor furni and 0 a wall furni (the item type is the class id). A server that
 * still sends the reward by name (product 0 with a word as the item type) is read the old way.
 */
export const getDailyTaskRewardIcon = (reward: Pick<DailyTaskData['rewards'][number], 'productItemTypeId' | 'rewardTypeId' | 'extraParams'>): DailyTaskRewardIcon => {
    const itemType = (reward?.rewardTypeId ?? '').trim();
    const numeric = /^-?\d+$/.test(itemType);

    if (reward?.productItemTypeId === 0 && !numeric) {
        const name = itemType.toLowerCase();

        if (name in LEGACY_DAILY_TASK_CURRENCIES) return { kind: 'currency', type: LEGACY_DAILY_TASK_CURRENCIES[name] };
        if (name === 'badge' && reward.extraParams) return { kind: 'badge', code: reward.extraParams };

        return null;
    }

    switch (reward?.productItemTypeId) {
        case 8:
            return numeric ? { kind: 'currency', type: Number(itemType) } : null;
        case 4:
            return itemType ? { kind: 'badge', code: itemType } : null;
        case 1:
        case 11:
            return numeric ? { kind: 'furni', productType: 's', classId: Number(itemType) } : null;
        case 0:
            return numeric ? { kind: 'furni', productType: 'i', classId: Number(itemType) } : null;
        default:
            return null;
    }
};
