import * as CompileConfig from "../config.json";
import * as invidiousList from "../ci/invidiouslist.json";
import { Category, CategorySelection, CategorySkipOption, NoticeVisibilityMode, PreviewBarOption, SponsorHideType, SponsorTime, VideoID, SegmentListDefaultTab } from "./types";
import { Keybind, keybindEquals, ProtoConfig } from "../maze-utils/src/config";
import type { HashedValue } from "../maze-utils/src/hash";
import { AdvancedSkipCheck, AdvancedSkipPredicate, AdvancedSkipRule, Permission, PredicateOperator } from "./utils/skipRule.type";

interface SBConfig {
    userID: string;
    isVip: boolean;
    permissions: Record<Category, Permission>;
    defaultCategory: Category;
    segmentListDefaultTab: SegmentListDefaultTab;
    renderSegmentsAsChapters: boolean;
    forceChannelCheck: boolean;
    minutesSaved: number;
    skipCount: number;
    sponsorTimesContributed: number;
    submissionCountSinceCategories: number; // New count used to show the "Read The Guidelines!!" message
    showTimeWithSkips: boolean;
    disableSkipping: boolean;
    muteSegments: boolean;
    fullVideoSegments: boolean;
    fullVideoLabelsOnThumbnails: boolean;
    manualSkipOnFullVideo: boolean;
    trackViewCount: boolean;
    trackViewCountInPrivate: boolean;
    trackDownvotes: boolean;
    trackDownvotesInPrivate: boolean;
    dontShowNotice: boolean;
    showUpcomingNotice: boolean;
    noticeVisibilityMode: NoticeVisibilityMode;
    hideVideoPlayerControls: boolean;
    hideInfoButtonPlayerControls: boolean;
    hideDeleteButtonPlayerControls: boolean;
    hideUploadButtonPlayerControls: boolean;
    hideSkipButtonPlayerControls: boolean;
    hideDiscordLaunches: number;
    hideDiscordLink: boolean;
    invidiousInstances: string[];
    supportInvidious: boolean;
    serverAddress: string;
    minDuration: number;
    skipNoticeDuration: number;
    audioNotificationOnSkip: boolean;
    checkForUnlistedVideos: boolean;
    testingServer: boolean;
    ytInfoPermissionGranted: boolean;
    allowExpirements: boolean;
    showDonationLink: boolean;
    showPopupDonationCount: number;
    showUpsells: boolean;
    showNewFeaturePopups: boolean;
    donateClicked: number;
    autoHideInfoButton: boolean;
    autoSkipOnMusicVideos: boolean;
    skipNonMusicOnlyOnYoutubeMusic: boolean;
    colorPalette: {
        red: string;
        white: string;
        locked: string;
    };
    scrollToEditTimeUpdate: boolean;
    categoryPillUpdate: boolean;
    hookUpdate: boolean;
    showChapterInfoMessage: boolean;
    darkMode: boolean;
    showCategoryGuidelines: boolean;
    showCategoryWithoutPermission: boolean;
    showSegmentNameInChapterBar: boolean;
    showAutogeneratedChapters: boolean;
    useVirtualTime: boolean;
    showSegmentFailedToFetchWarning: boolean;
    allowScrollingToEdit: boolean;
    deArrowInstalled: boolean;
    showDeArrowPromotion: boolean;
    showDeArrowInSettings: boolean;
    shownDeArrowPromotion: boolean;
    showZoomToFillError2: boolean;
    cleanPopup: boolean;
    hideSegmentCreationInPopup: boolean;
    prideTheme: boolean;

    // Used to cache calculated text color info
    categoryPillColors: {
        [key in Category]: {
            lastColor: string;
            textColor: string;
        }
    };

    skipKeybind: Keybind;
    skipToHighlightKeybind: Keybind;
    startSponsorKeybind: Keybind;
    submitKeybind: Keybind;
    actuallySubmitKeybind: Keybind;
    previewKeybind: Keybind;
    nextChapterKeybind: Keybind;
    previousChapterKeybind: Keybind;
    closeSkipNoticeKeybind: Keybind;
    upvoteKeybind: Keybind;
    downvoteKeybind: Keybind;

    // What categories should be skipped
    categorySelections: CategorySelection[];

    payments: {
        licenseKey: string;
        lastCheck: number;
        lastFreeCheck: number;
        freeAccess: boolean;
        chaptersAllowed: boolean;
    };

    // Preview bar
    barTypes: {
        "preview-chooseACategory": PreviewBarOption;
        "sponsor": PreviewBarOption;
        "preview-sponsor": PreviewBarOption;
        "selfpromo": PreviewBarOption;
        "preview-selfpromo": PreviewBarOption;
        "exclusive_access": PreviewBarOption;
        "interaction": PreviewBarOption;
        "preview-interaction": PreviewBarOption;
        "intro": PreviewBarOption;
        "preview-intro": PreviewBarOption;
        "outro": PreviewBarOption;
        "preview-outro": PreviewBarOption;
        "preview": PreviewBarOption;
        "preview-preview": PreviewBarOption;
        "music_offtopic": PreviewBarOption;
        "preview-music_offtopic": PreviewBarOption;
        "poi_highlight": PreviewBarOption;
        "preview-poi_highlight": PreviewBarOption;
        "filler": PreviewBarOption;
        "preview-filler": PreviewBarOption;
    };
}

export type VideoDownvotes = { segments: { uuid: HashedValue; hidden: SponsorHideType }[]; lastAccess: number };

export type ConfigurationID = string & { __configurationID: never };

export interface CustomConfiguration {
    name: string;
    categorySelections: CategorySelection[];
    showAutogeneratedChapters: boolean | null;
    autoSkipOnMusicVideos: boolean | null;
    skipNonMusicOnlyOnYoutubeMusic: boolean | null;
    muteSegments: boolean | null;
    fullVideoSegments: boolean | null;
    manualSkipOnFullVideo: boolean | null;
    minDuration: number | null;
}

interface SBStorage {
    /* VideoID prefixes to UUID prefixes */
    downvotedSegments: Record<VideoID & HashedValue, VideoDownvotes>;
    navigationApiAvailable: boolean;

    // Used when sync storage disabled
    alreadyInstalled: boolean;

    /* Contains unsubmitted segments that the user has created. */
    unsubmittedSegments: Record<string, SponsorTime[]>;

    channelSkipProfileIDs: Record<string, ConfigurationID>;
    skipProfileTemp: { time: number; configID: ConfigurationID } | null;
    skipProfiles: Record<ConfigurationID, CustomConfiguration>;

    skipRules: AdvancedSkipRule[];
}

class ConfigClass extends ProtoConfig<SBConfig, SBStorage> {
    resetToDefault() {
        chrome.storage.sync.set({
            ...this.syncDefaults,
            userID: this.config.userID,
            minutesSaved: this.config.minutesSaved,
            skipCount: this.config.skipCount,
            sponsorTimesContributed: this.config.sponsorTimesContributed
        });

        chrome.storage.local.set({
            ...this.localDefaults,
        });
    }
}

function migrateOldSyncFormats(config: SBConfig, local: SBStorage) {
    if (local["skipRules"] && local["skipRules"].length !== 0 && local["skipRules"][0]["rules"]) {
        const output: AdvancedSkipRule[] = [];

        for (const rule of local["skipRules"]) {
            const rules: object[] = rule["rules"];

            if (rules.length !== 0) {
                let predicate: AdvancedSkipPredicate = {
                    kind: "check",
                    ...rules[0] as AdvancedSkipCheck,
                };

                for (let i = 1; i < rules.length; i++) {
                    predicate = {
                        kind: "operator",
                        operator: PredicateOperator.And,
                        left: predicate,
                        right: {
                            kind: "check",
                            ...rules[i] as AdvancedSkipCheck,
                        },
                    };
                }

                const comment = rule["comment"] as string;

                output.push({
                    predicate,
                    skipOption: rule.skipOption,
                    comments: comment.length === 0 ? [] : comment.split(/;\s*/),
                });
            }
        }

        local["skipRules"] = output;
    }

    if (config["whitelistedChannels"]) {
        // convert to skipProfiles
        const whitelistedChannels = config["whitelistedChannels"] as string[];
        const skipProfileID: ConfigurationID = "default-whitelist" as ConfigurationID;

        local.skipProfiles[skipProfileID] = {
            name: chrome.i18n.getMessage("WhitelistedChannels"),
            categorySelections: config.categorySelections
                .filter((s) => !["exclusive_access", "chapter"].includes(s.name))
                .map(s => ({
                    name: s.name,
                    option: CategorySkipOption.ShowOverlay
            })),
            showAutogeneratedChapters: null,
            autoSkipOnMusicVideos: null,
            skipNonMusicOnlyOnYoutubeMusic: null,
            muteSegments: null,
            fullVideoSegments: null,
            manualSkipOnFullVideo: null,
            minDuration: null
        };
        local.skipProfiles = local.skipProfiles;

        for (const channelID of whitelistedChannels) {
            local.channelSkipProfileIDs[channelID] = skipProfileID;
        }
        local.channelSkipProfileIDs = local.channelSkipProfileIDs;

        chrome.storage.sync.remove("whitelistedChannels");
    }

    if (!config["changeChapterColor"]) {
        config.barTypes["chapter"].color = "#ffd983";
        config["changeChapterColor"] = true;
        chrome.storage.sync.set({
            "changeChapterColor": true,
            "barTypes": config.barTypes
        });
    }

    if (config["showZoomToFillError"]) {
        chrome.storage.sync.remove("showZoomToFillError");
    }

    if (config["unsubmittedSegments"] && Object.keys(config["unsubmittedSegments"]).length > 0) {
        chrome.storage.local.set({
            unsubmittedSegments: config["unsubmittedSegments"]
        }, () => {
            chrome.storage.sync.remove("unsubmittedSegments");
        });
    }

    if (!config["chapterCategoryAdded"]) {
        config["chapterCategoryAdded"] = true;

        if (!config.categorySelections.some((s) => s.name === "chapter")) {
            config.categorySelections.push({
                name: "chapter" as Category,
                option: CategorySkipOption.ShowOverlay
            });

            config.categorySelections = config.categorySelections;
        }
    }

    if (config["exclusive_accessCategoryAdded"] !== undefined) {
        chrome.storage.sync.remove("exclusive_accessCategoryAdded");
    }

    if (config["fillerUpdate"] !== undefined) {
        chrome.storage.sync.remove("fillerUpdate");
    }
    if (config["highlightCategoryAdded"] !== undefined) {
        chrome.storage.sync.remove("highlightCategoryAdded");
    }
    if (config["highlightCategoryUpdate"] !== undefined) {
        chrome.storage.sync.remove("highlightCategoryUpdate");
    }

    if (config["askAboutUnlistedVideos"]) {
        chrome.storage.sync.remove("askAboutUnlistedVideos");
    }

    if (!config["autoSkipOnMusicVideosUpdate"]) {
        config["autoSkipOnMusicVideosUpdate"] = true;
        for (const selection of config.categorySelections) {
            if (selection.name === "music_offtopic"
                && selection.option === CategorySkipOption.AutoSkip) {

                config.autoSkipOnMusicVideos = true;
                break;
            }
        }
    }

    if (config["disableAutoSkip"]) {
        for (const selection of config.categorySelections) {
            if (selection.name === "sponsor") {
                selection.option = CategorySkipOption.ManualSkip;

                chrome.storage.sync.remove("disableAutoSkip");
            }
        }
    }

    if (typeof config["skipKeybind"] == "string") {
        config["skipKeybind"] = { key: config["skipKeybind"] };
    }

    if (typeof config["startSponsorKeybind"] == "string") {
        config["startSponsorKeybind"] = { key: config["startSponsorKeybind"] };
    }

    if (typeof config["submitKeybind"] == "string") {
        config["submitKeybind"] = { key: config["submitKeybind"] };
    }

    // Unbind key if it matches a previous one set by the user (should be ordered oldest to newest)
    const keybinds = ["skipKeybind", "startSponsorKeybind", "submitKeybind"];
    for (let i = keybinds.length - 1; i >= 0; i--) {
        for (let j = 0; j < keybinds.length; j++) {
            if (i == j)
                continue;
            if (keybindEquals(config[keybinds[i]], config[keybinds[j]]))
                config[keybinds[i]] = null;
        }
    }

    // Remove some old unused options
    if (config["sponsorVideoID"] !== undefined) {
        chrome.storage.sync.remove("sponsorVideoID");
    }
    if (config["previousVideoID"] !== undefined) {
        chrome.storage.sync.remove("previousVideoID");
    }

    // populate invidiousInstances with new instances if 3p support is **DISABLED**
    if (!config["supportInvidious"] && config["invidiousInstances"].length < invidiousList.length) {
        config["invidiousInstances"] = [...new Set([...invidiousList, ...config["invidiousInstances"]])];
    }

    if (config["lastIsVipUpdate"]) {
        chrome.storage.sync.remove("lastIsVipUpdate");
    }
}

const syncDefaults = {
    userID: null,
    isVip: false,
    permissions: {},
    defaultCategory: "chooseACategory" as Category,
    segmentListDefaultTab: SegmentListDefaultTab.Segments,
    renderSegmentsAsChapters: false,
    forceChannelCheck: false,
    minutesSaved: 0,
    skipCount: 0,
    sponsorTimesContributed: 0,
    submissionCountSinceCategories: 0,
    showTimeWithSkips: true,
    disableSkipping: false,
    muteSegments: true,
    fullVideoSegments: true,
    fullVideoLabelsOnThumbnails: true,
    manualSkipOnFullVideo: false,
    trackViewCount: true,
    trackViewCountInPrivate: true,
    trackDownvotes: true,
    trackDownvotesInPrivate: false,
    dontShowNotice: false,
    showUpcomingNotice: false,
    noticeVisibilityMode: NoticeVisibilityMode.FadedForAutoSkip,
    hideVideoPlayerControls: false,
    hideInfoButtonPlayerControls: false,
    hideDeleteButtonPlayerControls: false,
    hideUploadButtonPlayerControls: false,
    hideSkipButtonPlayerControls: false,
    hideDiscordLaunches: 0,
    hideDiscordLink: false,
    invidiousInstances: [],
    supportInvidious: false,
    serverAddress: CompileConfig.serverAddress,
    minDuration: 0,
    skipNoticeDuration: 4,
    audioNotificationOnSkip: false,
    checkForUnlistedVideos: false,
    testingServer: false,
    ytInfoPermissionGranted: false,
    allowExpirements: true,
    showDonationLink: true,
    showPopupDonationCount: 0,
    showUpsells: true,
    showNewFeaturePopups: true,
    donateClicked: 0,
    autoHideInfoButton: true,
    autoSkipOnMusicVideos: false,
    skipNonMusicOnlyOnYoutubeMusic: false,
    scrollToEditTimeUpdate: false, // false means the tooltip will be shown
    categoryPillUpdate: false,
    hookUpdate: false,
    showChapterInfoMessage: true,
    darkMode: true,
    showCategoryGuidelines: true,
    showCategoryWithoutPermission: false,
    showSegmentNameInChapterBar: true,
    showAutogeneratedChapters: true,
    useVirtualTime: true,
    showSegmentFailedToFetchWarning: true,
    allowScrollingToEdit: true,
    deArrowInstalled: false,
    showDeArrowPromotion: true,
    showDeArrowInSettings: true,
    shownDeArrowPromotion: false,
    showZoomToFillError2: true,
    cleanPopup: false,
    hideSegmentCreationInPopup: false,
    prideTheme: false,

    categoryPillColors: {},

    /**
     * Default keybinds should not set "code" as that's gonna be different based on the user's locale. They should also only use EITHER ctrl OR alt modifiers (or none).
     * Using ctrl+alt, or shift may produce a different character that we will not be able to recognize in different locales.
     * The exception for shift is letters, where it only capitalizes. So shift+A is fine, but shift+1 isn't.
     * Don't forget to add the new keybind to the checks in "KeybindDialogComponent.isKeybindAvailable()" and in "migrateOldFormats()"!
     *      TODO: Find a way to skip having to update these checks. Maybe storing keybinds in a Map?
     */
    skipKeybind: { key: "Enter" },
    skipToHighlightKeybind: { key: "Enter", ctrl: true },
    startSponsorKeybind: { key: ";" },
    submitKeybind: { key: "'" },
    actuallySubmitKeybind: { key: "'", ctrl: true },
    previewKeybind: { key: ";", ctrl: true },
    nextChapterKeybind: { key: "ArrowRight", ctrl: true },
    previousChapterKeybind: { key: "ArrowLeft", ctrl: true },
    closeSkipNoticeKeybind: { key: "Backspace" },
    downvoteKeybind: { key: "h", shift: true },
    upvoteKeybind: { key: "g", shift: true },

    categorySelections: [{
        name: "sponsor" as Category,
        option: CategorySkipOption.AutoSkip
    }, {
        name: "poi_highlight" as Category,
        option: CategorySkipOption.ManualSkip
    }, {
        name: "exclusive_access" as Category,
        option: CategorySkipOption.ShowOverlay
    }, {
        name: "chapter" as Category,
        option: CategorySkipOption.ShowOverlay
    }],

    payments: {
        licenseKey: null,
        lastCheck: 0,
        lastFreeCheck: 0,
        freeAccess: false,
        chaptersAllowed: false
    },

    colorPalette: {
        red: "#780303",
        white: "#ffffff",
        locked: "#ffc83d"
    },

    // Preview bar
    barTypes: {
        "preview-chooseACategory": {
            color: "#ffffff",
            opacity: "0.7"
        },
        "sponsor": {
            color: "#00d400",
            opacity: "0.7"
        },
        "preview-sponsor": {
            color: "#007800",
            opacity: "0.7"
        },
        "selfpromo": {
            color: "#ffff00",
            opacity: "0.7"
        },
        "preview-selfpromo": {
            color: "#bfbf35",
            opacity: "0.7"
        },
        "exclusive_access": {
            color: "#008a5c",
            opacity: "0.7"
        },
        "interaction": {
            color: "#cc00ff",
            opacity: "0.7"
        },
        "preview-interaction": {
            color: "#6c0087",
            opacity: "0.7"
        },
        "intro": {
            color: "#00ffff",
            opacity: "0.7"
        },
        "preview-intro": {
            color: "#008080",
            opacity: "0.7"
        },
        "outro": {
            color: "#0202ed",
            opacity: "0.7"
        },
        "preview-outro": {
            color: "#000070",
            opacity: "0.7"
        },
        "preview": {
            color: "#008fd6",
            opacity: "0.7"
        },
        "preview-preview": {
            color: "#005799",
            opacity: "0.7"
        },
        "hook": {
            color: "#395699",
            opacity: "0.8"
        },
        "preview-hook": {
            color: "#273963",
            opacity: "0.7"
        },
        "music_offtopic": {
            color: "#ff9900",
            opacity: "0.7"
        },
        "preview-music_offtopic": {
            color: "#a6634a",
            opacity: "0.7"
        },
        "poi_highlight": {
            color: "#ff1684",
            opacity: "0.7"
        },
        "preview-poi_highlight": {
            color: "#9b044c",
            opacity: "0.7"
        },
        "filler": {
            color: "#7300FF",
            opacity: "0.9"
        },
        "preview-filler": {
            color: "#2E0066",
            opacity: "0.7"
        },
        "chapter": {
            color: "#ffd983",
            opacity: "0"
        },
    }
};

const localDefaults = {
    downvotedSegments: {},
    navigationApiAvailable: null,
    alreadyInstalled: false,

    unsubmittedSegments: {},
    skipRules: [],

    channelSkipProfileIDs: {},
    skipProfiles: {},
    skipProfileTemp: null
};

const Config = new ConfigClass(syncDefaults, localDefaults, migrateOldSyncFormats);
export default Config;

export function generateDebugDetails(): string {
    // Build output debug information object
    const output = {
        debug: {
            userAgent: navigator.userAgent,
            platform: navigator.platform,
            language: navigator.language,
            extensionVersion: chrome.runtime.getManifest().version
        },
        config: JSON.parse(JSON.stringify(Config.cachedSyncConfig)) // Deep clone config object
    };

    // Sanitise sensitive user config values
    delete output.config.userID;
    output.config.serverAddress = (output.config.serverAddress === CompileConfig.serverAddress)
        ? "Default server address" : "Custom server address";
    output.config.invidiousInstances = output.config.invidiousInstances.length;
    output.config.skipRules = output.config.skipRules.length;

    return JSON.stringify(output, null, 4);
}
