import { PluginSettingsManagerBase } from "obsidian-dev-utils/obsidian/plugin/plugin-settings-manager-base";
import type InstaTocPlugin from "../Plugin";
import type { FileKey, FoldKey, PluginTypes } from "../types";
import type UiStateManager from "../uiStateManager";
import { DEFAULT_SETTINGS, type InstaTocPersistedData, type InstaTocSettings } from "./Settings";

type LoadedPersistedData = {
    settings: Partial<InstaTocSettings>;
    tocFoldState: Map<FoldKey, boolean>;
    tocBlockCollapseState: Map<FileKey, boolean>;
};

export class PluginSettingsManager extends PluginSettingsManagerBase<PluginTypes> {
    private _uiStateManager: UiStateManager | undefined;

    public constructor(plugin: InstaTocPlugin) {
        super(plugin);
    }

    private get uiStateManager(): UiStateManager {
        if (!this._uiStateManager) {
            this._uiStateManager = this.plugin.uiStateManager;
        }
        return this._uiStateManager;
    }

    protected override createDefaultSettings(): InstaTocSettings {
        return {
            ...DEFAULT_SETTINGS,
            excludedChars: [ ...DEFAULT_SETTINGS.excludedChars ],
            excludedHeadingLevels: [ ...DEFAULT_SETTINGS.excludedHeadingLevels ],
            excludedHeadingText: [ ...DEFAULT_SETTINGS.excludedHeadingText ]
        };
    }

    protected override async onLoadRecord(rawRecord: Record<string, unknown>): Promise<void> {
        const persistedData = this.parsePersistedData(rawRecord);

        this.uiStateManager.setPersistedUiState(persistedData.tocFoldState, persistedData.tocBlockCollapseState);

        // Convert persisted shape -> flat settings record expected by base manager
        for (const key of Object.keys(rawRecord)) {
            delete rawRecord[key];
        }

        Object.assign(rawRecord, persistedData.settings);
    }

    protected override async onSavingRecord(rawRecord: Record<string, unknown>): Promise<void> {
        const persistedData = this.buildPersistedData({ ...rawRecord } as Partial<InstaTocSettings>);

        for (const key of Object.keys(rawRecord)) {
            delete rawRecord[key];
        }

        Object.assign(rawRecord, persistedData as Record<string, unknown>);
    }

    public async savePersistedData(): Promise<void> {
        await this.plugin.saveData(
            this.buildPersistedData(this.settingsWrapper.settings as InstaTocSettings) as Record<string, unknown>
        );
    }

    private buildPersistedData(settings: Partial<InstaTocSettings>): InstaTocPersistedData {
        const { tocBlockCollapseState, tocFoldState } = this.uiStateManager.getPersistedUiState();

        return {
            settings: { ...settings },
            tocBlockCollapseState: this.stateMapToRecord(tocBlockCollapseState),
            tocFoldState: this.stateMapToRecord(tocFoldState, ([ a ], [ b ]) => {
                const [ aPath, aIndex ] = a.split("::") as [FileKey, `${number}`];
                const [ bPath, bIndex ] = b.split("::") as [FileKey, `${number}`];
                const aSortable = `${aPath}::${Number(aIndex).toString().padStart(2, "0")}`;
                const bSortable = `${bPath}::${Number(bIndex).toString().padStart(2, "0")}`;

                return aSortable.localeCompare(bSortable);
            })
        };
    }

    public override registerValidators(): void {
        super.registerValidators();

        this.registerValidator("updateDelay", (value) => {
            if (value < 500 || value > 10000 || value % 500 !== 0) {
                return "Update delay must be between 500 and 10000 milliseconds in 500 ms steps.";
            }
        });

        this.registerValidator("tocTitleLevel", (value) => {
            if (value < 1 || value > 6) {
                return "ToC heading level must be between 1 and 6.";
            }
        });

        this.registerValidator("excludedHeadingLevels", (value) => {
            if (value.some((level) => level < 1 || level > 6)) {
                return "Excluded heading levels must contain only values from 1 to 6.";
            }
        });

        this.registerValidator("excludedHeadingText", (value) => {
            if (value.some((item) => item.trim() === "")) {
                return "Excluded heading text cannot contain empty values.";
            }
        });

        this.registerValidator("excludedChars", (value) => {
            if (value.some((item) => item.trim() === "")) {
                return "Excluded characters cannot contain empty values.";
            }
        });
    }

    private parsePersistedData(data: unknown): LoadedPersistedData {
        if (!isRecord(data)) {
            return {
                settings: {},
                tocBlockCollapseState: new Map<FileKey, boolean>(),
                tocFoldState: new Map<FoldKey, boolean>()
            };
        }

        return {
            settings: isRecord<Partial<InstaTocSettings>>(data.settings) ? (data.settings) : {},
            tocBlockCollapseState: this.recordToStateMap<FileKey>(data.tocBlockCollapseState),
            tocFoldState: this.recordToStateMap<FoldKey>(data.tocFoldState)
        };
    }

    private recordToStateMap<T extends FoldKey | FileKey>(rawState: unknown): Map<T, boolean> {
        const state: Map<T, boolean> = new Map();

        if (isRecord(rawState)) {
            for (const [ key, value ] of Object.entries(rawState)) {
                if (typeof value === "boolean") {
                    state.set(key as T, value);
                }
            }
        }

        return state;
    }

    private stateMapToRecord<T extends FoldKey | FileKey>(
        state: Map<T, boolean>,
        sortFn?: (a: [T, boolean], b: [T, boolean]) => number
    ): Record<T, boolean> {
        return Object.fromEntries(sortMap(state, sortFn)) as Record<T, boolean>;
    }
}
