import {createStore} from 'vuex';
import router from '@/router';
import FallBackResolver from "@/dns";
import NeighborsCache from "@/neigbors";
import {createNullAuth, createSignAuth, createTokenAuth, ServerSet, ServerSetUnion} from "@/federation";
import {parseIdentityRecord, serializeIdentityRecord} from "@/identity";
import {serializeWorkflowPayload, deserializeWorkflowPayload} from "@/workflows.js";
//import sharedStatePlugin from "@/../extras/shared-state-plugin";
//import persistentStatePlugin from "@/../extras/persistent-state-plugin";

const defaultPreferenceDefinitions = [
    {
        key: 'ui.compact_mode',
        type: 'boolean',
        default: false,
        label: 'Compact mode',
        description: 'Show denser item rows and reduce spacing in lists.'
    },
    {
        key: 'ui.default_search_scope',
        type: 'enum',
        options: ['inventory', 'friends', 'all'],
        default: 'inventory',
        label: 'Default search scope',
        description: 'Choose where the global search starts.'
    },
    {
        key: 'notifications.desktop_enabled',
        type: 'boolean',
        default: true,
        label: 'Desktop notifications',
        description: 'Enable in-browser notifications for important updates.'
    },
    {
        key: 'files.max_upload_mb',
        type: 'integer',
        default: 25,
        label: 'Default upload size limit (MB)',
        description: 'Used as a prefill hint in upload dialogs.'
    },
    {
        key: 'ui.experimental_flags',
        type: 'json',
        default: {},
        label: 'Experimental flags',
        description: 'Optional JSON toggles for feature previews.'
    },
]

const parseStoredPreferences = (storageKey) => {
    try {
        const raw = localStorage.getItem(storageKey)
        if (!raw) {
            return {}
        }
        const parsed = JSON.parse(raw)
        return parsed && typeof parsed === 'object' ? parsed : {}
    } catch (_error) {
        return {}
    }
}


export default createStore({
    state: {
        local_loaded: false,
        last_load: {},
        user: null,
        user_profile: null,
        token: null,
        keypair: null,
        remember: false,
        friends: [],
        friendProfiles: {},
        groups: [],
        groupInvites: [],
        idmap: {identities: [], groups: []},
        idmapLoaded: false,
        item_map: {},
        home_servers: null,
        all_friends_servers: null,
        messages: [],
        resolver: new FallBackResolver(),
        unreachable_neighbors: new NeighborsCache(),
        tags: [],
        properties: [],
        files: [],
        categories: [],
        availability_policies: [],
        domains: [],
        storage_locations: [],
        active_workflows: [],
        preferenceDefinitions: [],
        accountPreferences: {},
        devicePreferences: {},
        preferencesLoaded: false,
    },
    mutations: {
        setInventoryItems(state, {url, items}) {
            state.item_map[url] = items;
        },
        setFriends(state, friends) {
            state.friends = friends;
        },
        setFriendProfile(state, {username, profile}) {
            state.friendProfiles = {...state.friendProfiles, [username]: profile};
        },
        setFriendProfiles(state, profiles) {
            state.friendProfiles = profiles;
        },
        setGroups(state, groups) {
            state.groups = groups;
        },
        setGroupInvites(state, invites) {
            state.groupInvites = invites;
        },
        setIdMap(state, idmap) {
            state.idmap = idmap;
            state.idmapLoaded = true;
        },
        setHomeServers(state, home_servers) {
            state.home_servers = home_servers;
        },
        setAllFriendsServers(state, servers) {
            state.all_friends_servers = servers;
        },
        setTags(state, tags) {
            state.tags = tags;
        },
        setProperties(state, properties) {
            state.properties = properties;
        },
        setCategories(state, categories) {
            state.categories = categories;
        },
        setAvailabilityPolicies(state, availability_policies) {
            state.availability_policies = availability_policies;
        },
        setDomains(state, domains) {
            state.domains = domains;
        },
        setStorageLocations(state, storage_locations) {
            state.storage_locations = storage_locations;
        },
        setFiles(state, files) {
            state.files = files;
        },
        setActiveWorkflows(state, workflows) {
            state.active_workflows = workflows;
        },
        setPreferenceDefinitions(state, definitions) {
            state.preferenceDefinitions = definitions;
        },
        setAccountPreferences(state, preferences) {
            state.accountPreferences = preferences;
        },
        setDevicePreferences(state, preferences) {
            state.devicePreferences = preferences;
        },
        setAccountPreference(state, {key, value}) {
            state.accountPreferences = {...state.accountPreferences, [key]: value};
        },
        setDevicePreference(state, {key, value}) {
            state.devicePreferences = {...state.devicePreferences, [key]: value};
        },
        deleteAccountPreference(state, key) {
            const prefs = {...state.accountPreferences};
            delete prefs[key];
            state.accountPreferences = prefs;
        },
        deleteDevicePreference(state, key) {
            const prefs = {...state.devicePreferences};
            delete prefs[key];
            state.devicePreferences = prefs;
        },
        setPreferencesLoaded(state, loaded) {
            state.preferencesLoaded = loaded;
        },
        setUser(state, user) {
            state.user = user;
            if (state.remember)
                localStorage.setItem('user', user);
        },
        setUserProfile(state, profile) {
            state.user_profile = profile;
        },
        setToken(state, token) {
            state.token = token;
            if (state.remember)
                localStorage.setItem('token', token);
        },
        setKey(state, keypair) {
            state.keypair = nacl.crypto_sign_keypair_from_seed(nacl.from_hex(keypair))
            if (state.remember)
                localStorage.setItem('keypair', nacl.to_hex(state.keypair.signSk).slice(0, 64))
        },
        setRemember(state, remember) {
            state.remember = remember;
            if (!remember) {
                localStorage.removeItem('user');
                localStorage.removeItem('token');
                localStorage.removeItem('keypair');
            }
            localStorage.setItem('remember', remember);
        },
        setUserFromIdentityRecord(state, {user, keypair}) {
            try {
                localStorage.removeItem('user');
                localStorage.removeItem('token');
                localStorage.removeItem('keypair');
                state.user = user;
                state.keypair = keypair;
                if (state.remember) {
                    localStorage.setItem('user', user);
                    localStorage.setItem('keypair', key)
                }
            } catch (e) {
                console.error('Invalid identity record')
            }
        },
        logout(state) {
            state.user = null;
            state.user_profile = null;
            state.token = null;
            state.keypair = null;
            state.accountPreferences = {};
            state.preferenceDefinitions = [];
            state.preferencesLoaded = false;
            // Note: devicePreferences are NOT cleared on logout (device-specific)
            localStorage.removeItem('user');
            localStorage.removeItem('token');
            localStorage.removeItem('keypair');
            router.push('/login');
        },
        load_local(state) {
            if (state.local_loaded)
                return;
            const remember = localStorage.getItem('remember');
            const user = localStorage.getItem('user');
            const token = localStorage.getItem('token');
            const keypair = localStorage.getItem('keypair');
            if (user && token) {
                this.commit('setUser', user);
                this.commit('setToken', token);
                if (keypair) {
                    this.commit('setKey', keypair)
                }
                router.push('/');
            }
            state.cache_loaded = true;
        }
    },
    actions: {
        async loadUserPreferences({state, commit, dispatch, getters}) {
            if (state.preferencesLoaded) return
            const deviceKey = 'toolshed.preferences.device'
            commit('setDevicePreferences', parseStoredPreferences(deviceKey))

            let definitions = defaultPreferenceDefinitions
            let accountPreferences = {}
            try {
                const servers = await dispatch('getHomeServers')
                definitions = await servers.get(getters.nullAuth, '/auth/preferences/') || defaultPreferenceDefinitions
                accountPreferences = await servers.get(getters.signAuth, '/auth/self/preferences/') || {}
            } catch (error) {
                console.error('Failed to load user preferences:', error)
            }
            commit('setPreferenceDefinitions', definitions)
            commit('setAccountPreferences', accountPreferences)
            commit('setPreferencesLoaded', true)
        },
        async setAccountPreference({state, commit, dispatch, getters}, {key, value}) {
            const servers = await dispatch('getHomeServers')
            await servers.put(getters.signAuth, '/auth/self/preferences/', {[key]: value})
            commit('setAccountPreference', {key, value})
        },
        async resetAccountPreference({state, commit, dispatch, getters}, key) {
            const servers = await dispatch('getHomeServers')
            await servers.delete(getters.signAuth, '/auth/self/preferences/' + key + '/')
            commit('deleteAccountPreference', key)
        },
        async setDevicePreference({state, commit}, {key, value}) {
            commit('setDevicePreference', {key, value})
            localStorage.setItem('toolshed.preferences.device', JSON.stringify(state.devicePreferences))
        },
        async resetDevicePreference({state, commit}, key) {
            commit('deleteDevicePreference', key)
            localStorage.setItem('toolshed.preferences.device', JSON.stringify(state.devicePreferences))
        },
        async login({commit, dispatch, state, getters}, {username, password, remember}) {
            commit('setRemember', remember);
            const data = await dispatch('lookupServer', {username}).then(servers => new ServerSet(servers, state.unreachable_neighbors))
                .then(set => set.post(getters.nullAuth, '/auth/token/', {username, password}))
            if (data.token && data.key) {
                commit('setToken', data.token);
                commit('setUser', username);
                commit('setKey', data.key);
                const s = await dispatch('lookupServer', {username}).then(servers => new ServerSet(servers, state.unreachable_neighbors))
                commit('setHomeServers', s)
                await dispatch('fetchUserProfile', {force: true})
                return true;
            } else {
                return false;
            }
        },
        async fetchUserProfile({state, commit, dispatch, getters}, {force = false} = {}) {
            if (!force && state.user_profile && state.last_load.user_profile > Date.now() - 1000 * 60) {
                return state.user_profile
            }
            const servers = await dispatch('getHomeServers')
            const data = await servers.get(getters.signAuth, '/auth/user/')
            commit('setUserProfile', data)
            state.last_load.user_profile = Date.now()
            return data
        },
        async updateUserProfilePicture({state, commit, dispatch, getters}, {file = null} = {}) {
            const servers = await dispatch('getHomeServers')
            const payload = file ? {profile_picture: {data: file.data, mime_type: file.mime_type}} : {profile_picture: null}
            const data = await servers.patch(getters.signAuth, '/auth/user/', payload)
            commit('setUserProfile', data)
            state.last_load.user_profile = Date.now()
            if (data.profile_picture) {
                state.last_load.files = 0
            }
            return data
        },
        async lookupServer({state}, {username}) {
            const domain = username.split('@')[1]
            const request = '_toolshed-server._tcp.' + domain + '.'
            return await state.resolver.query(request, 'SRV').then(
                (result) => result.map(
                    // Must match the browser's real Host header (federation.js signs/fetches
                    // "https://"+server+target); browser omits :443 for default HTTPS, so
                    // keeping it here would break signature checks on the receiving end.
                    (answer) => answer.port === 443 ? answer.target : answer.target + ':' + answer.port))
        },
        async getHomeServers({state, dispatch, commit, getters}) {
            if (state.home_servers)
                return state.home_servers
            // Reading isLoggedIn first forces its lazy hydration of state.user from localStorage,
            // needed here since routes without requiresAuth (e.g. short-id redirect) skip that
            // check; fail clearly if still not logged in rather than crashing on username.split.
            if (!getters.isLoggedIn) {
                throw new Error('Not logged in')
            }
            const promise = dispatch('lookupServer', {username: state.user}).then(servers => new ServerSet(servers, state.unreachable_neighbors))
            commit('setHomeServers', promise)
            return promise
        },
        async getAllKnownServers({state, dispatch, commit}) {
            const friends = await dispatch('fetchFriends')
            if (state.all_friends_servers)
                return state.all_friends_servers
            const promise = (async () => {
                const servers = new ServerSetUnion([])
                const home = await dispatch('getHomeServers')
                servers.add(home)
                for (const friend of friends) {
                    try {
                        const s = await dispatch('lookupServer', {username: friend.username})
                        servers.add(new ServerSet(s, state.unreachable_neighbors))
                    } catch (e) {
                        // Skip an unresolvable/unreachable friend rather than aborting the whole lookup.
                        console.error('could not resolve server for friend', friend.username, e)
                    }
                }
                return servers
            })().catch(e => {
                // Don't cache a permanently-rejected promise - allow a retry next time.
                state.all_friends_servers = null
                throw e
            })
            commit('setAllFriendsServers', promise)
            return promise
        },
        async getFriendServers({state, dispatch, commit}, {username}) {
            return dispatch('lookupServer', {username}).then(servers => new ServerSet(servers, state.unreachable_neighbors))
        },
        async fetchBackendVersion({dispatch, getters}) {
            const servers = await dispatch('getHomeServers')
            return await servers.get(getters.nullAuth, '/api/version/')
        },
        async fetchInventoryItems({commit, dispatch, getters}) {
            const servers = await dispatch('getHomeServers')
            const items = await servers.get(getters.signAuth, '/api/inventory_items/')
            items.map(item => item.files.map(file => file.owner = item.owner))
            commit('setInventoryItems', {url: '/', items})
            return items
        },
        async createInventoryItem({state, dispatch, getters}, item) {
            const servers = await dispatch('getHomeServers')
            const data = {availability_policy: 'private', ...item}
            const reply = await servers.post(getters.signAuth, '/api/inventory_items/', data)
            state.last_load.files = 0
            return reply
        },
        async updateInventoryItem({state, dispatch, getters}, item) {
            const servers = await dispatch('getHomeServers')
            const data = {availability_policy: 'friends', ...item}
            data.files = data.files.map(file => file.id)
            return await servers.patch(getters.signAuth, '/api/inventory_items/' + item.id + '/', data)
        },
        async deleteInventoryItem({state, dispatch, getters}, item) {
            const servers = await dispatch('getHomeServers')
            const ret = await servers.delete(getters.signAuth, '/api/inventory_items/' + item.id + '/')
            dispatch('fetchInventoryItems')
            return ret
        },
        async fetchSearchResults({state, commit, dispatch, getters}, {query}) {
            const servers = await dispatch('getAllKnownServers')
            const items = await servers.get(getters.signAuth, '/api/search/?query=' + query)
            items.map(item => item.files.map(file => file.owner = item.owner))
            commit('setInventoryItems', {url: '/search/' + query, items})
            return items
        },
        async fetchFriends({commit, dispatch, getters, state}) {
            const servers = await dispatch('getHomeServers')
            const data = await servers.get(getters.signAuth, '/api/friends/')
            commit('setFriends', data)
            return data
        },
        async fetchFriendProfile({commit, dispatch, getters}, {username}) {
            try {
                const servers = await dispatch('getFriendServers', {username});
                // username here is already a full handle (username@domain), see FriendSerializer
                const profile = await servers.get(getters.signAuth, '/auth/user/' + username + '/');
                if (profile) {
                    commit('setFriendProfile', {username, profile});
                }
                return profile;
            } catch (error) {
                console.error(`Failed to fetch profile for ${username}:`, error);
                return null;
            }
        },
        async fetchForeignItem({dispatch, getters}, {owner, id}) {
            try {
                const servers = await dispatch('getFriendServers', {username: owner});
                // owner is a full handle (username@domain); this endpoint looks the item up by
                // owner, not requester (see toolshed/api/inventory.py get_shared_item).
                const item = await servers.get(getters.signAuth, '/api/inventory_items/' + owner + '/' + id + '/');
                if (item && item.files) {
                    item.files.forEach(file => file.owner = item.owner)
                }
                return item;
            } catch (error) {
                console.error(`Failed to fetch item ${id} for ${owner}:`, error);
                return null;
            }
        },
        // Group handles have no owner-handle GET route yet, so they resolve differently than
        // personal handles here. See docs/implementation.md#fetch-item-by-handle-group-vs-personal-handles.
        async fetchItemByHandle({dispatch, getters}, {handle, id}) {
            if (handle.startsWith('#')) {
                // idmap isn't guaranteed loaded on a direct/refreshed visit here; fetchIdMap is
                // cheap and already called unconditionally by every other caller too.
                await dispatch('fetchIdMap')
                const groupId = getters.groupIdByHandle[handle]
                if (groupId === undefined) {
                    return null
                }
                const items = await dispatch('fetchGroupInventoryItems', groupId)
                return items.find(item => item.id === parseInt(id)) || null
            }
            return dispatch('fetchForeignItem', {owner: handle, id})
        },
        async fetchFriendRequests({state, dispatch, getters}) {
            const servers = await dispatch('getHomeServers')
            return await servers.get(getters.signAuth, '/api/friendrequests/')
        },
        async requestFriend({state, dispatch, getters}, {username}) {
            if (username in state.friends) {
                return true;
            }
            const home_servers = await dispatch('getHomeServers')
            const home_reply = await home_servers.post(getters.signAuth, '/api/friendrequests/', {
                befriender: state.user,
                befriendee: username
            })
            if (home_reply.status !== 'pending' || !home_reply.secret)
                return false;

            const befriendee_servers = await dispatch('getFriendServers', {username})
            const ext_reply = await befriendee_servers.post(getters.signAuth, '/api/friendrequests/', {
                befriender: state.user,
                befriendee: username,
                befriender_key: nacl.to_hex(state.keypair.signPk),
                secret: home_reply.secret
            })
            return true;
        },
        async acceptFriend({state, dispatch, getters}, {id, secret, befriender}) {
            const home_servers = await dispatch('getHomeServers')
            const home_reply = await home_servers.post(getters.signAuth, '/api/friends/', {
                friend_request_id: id, secret: secret
            })
            const ext_servers = await dispatch('getFriendServers', {username: befriender})
            const ext_reply = await ext_servers.post(getters.signAuth, '/api/friendrequests/', {
                befriender: state.user,
                befriendee: befriender,
                befriender_key: nacl.to_hex(state.keypair.signPk),
                secret: secret
            })
            return true
        },
        async declineFriend({state, dispatch, getters}, {id}) {
            const servers = await dispatch('getHomeServers')
            return await servers.delete(getters.signAuth, '/api/friendrequests/' + id + '/')
        },
        async dropFriend({state, dispatch, getters}, {id}) {
            const servers = await dispatch('getHomeServers')
            return await servers.delete(getters.signAuth, '/api/friends/' + id + '/')
        },
        // Groups are only ever hosted on the current user's own home backend for now (see
        // docs/design-in-progress/groups-mvp.md), so every group action below uses getHomeServers
        // rather than resolving a per-group domain.
        async fetchGroups({commit, dispatch, getters}) {
            const servers = await dispatch('getHomeServers')
            const data = await servers.get(getters.signAuth, '/api/groups/')
            commit('setGroups', data)
            return data
        },
        async fetchIdMap({commit, dispatch, getters}) {
            const servers = await dispatch('getHomeServers')
            const idmap = await servers.get(getters.signAuth, '/api/idmap/')
            commit('setIdMap', idmap)
            return idmap
        },
        async fetchGroup({dispatch, getters}, {id}) {
            const servers = await dispatch('getHomeServers')
            return await servers.get(getters.signAuth, '/api/groups/' + id + '/')
        },
        async createGroup({dispatch, getters}, {name}) {
            const servers = await dispatch('getHomeServers')
            return await servers.post(getters.signAuth, '/api/groups/', {name})
        },
        async removeGroupMember({dispatch, getters}, {groupId, identityId}) {
            const servers = await dispatch('getHomeServers')
            return await servers.delete(getters.signAuth, '/api/groups/' + groupId + '/members/' + identityId + '/')
        },
        async fetchGroupInvites({commit, dispatch, getters}) {
            const servers = await dispatch('getHomeServers')
            const data = await servers.get(getters.signAuth, '/api/groupinvites/')
            commit('setGroupInvites', data)
            return data
        },
        async inviteToGroup({state, dispatch, getters}, {groupId, groupHandle, invitee}) {
            const home_servers = await dispatch('getHomeServers')
            const home_reply = await home_servers.post(
                getters.signAuth, '/api/groups/' + groupId + '/invites/', {invitee})
            if (!home_reply.secret) {
                return false
            }
            const invitee_servers = await dispatch('getFriendServers', {username: invitee})
            await invitee_servers.post(getters.signAuth, '/api/groupinvites/', {
                group: groupHandle,
                inviter: state.user,
                inviter_key: nacl.to_hex(state.keypair.signPk),
                invitee,
                secret: home_reply.secret
            })
            return true
        },
        async acceptGroupInvite({state, dispatch, getters}, invite) {
            const group_domain = invite.group.split('@')[1]
            const group_servers = await dispatch('getFriendServers', {username: 'x@' + group_domain})
            await group_servers.post(getters.signAuth, '/api/group_invites/accept/', {
                group: invite.group,
                invitee: state.user,
                invitee_key: nacl.to_hex(state.keypair.signPk),
                secret: invite.secret
            })
            const home_servers = await dispatch('getHomeServers')
            await home_servers.delete(getters.signAuth, '/api/groupinvites/' + invite.id + '/')
            return true
        },
        async declineGroupInvite({dispatch, getters}, invite) {
            const servers = await dispatch('getHomeServers')
            return await servers.delete(getters.signAuth, '/api/groupinvites/' + invite.id + '/')
        },
        async fetchGroupInventoryItems({commit, dispatch, getters}, groupId) {
            const servers = await dispatch('getHomeServers')
            const items = await servers.get(getters.signAuth, '/api/inventory_items/?group=' + groupId)
            commit('setInventoryItems', {url: '/group/' + groupId, items})
            return items
        },
        async fetchFiles({state, commit, dispatch, getters}) {
            if (state.last_load.files > Date.now() - 1000 * 60 * 60 * 24) {
                return state.files
            }
            const servers = await dispatch('getHomeServers')
            const data = await servers.get(getters.signAuth, '/api/files/')
            data.map(file => file.owner = state.user)
            commit('setFiles', data)
            state.last_load.files = Date.now()
            return data
        },
        async fetchFilesByItem({state, commit, dispatch, getters}, {id}) {
            const servers = await dispatch('getHomeServers')
            return await servers.get(getters.signAuth, '/api/item_files/' + id + '/')
        },
        async pushFile({state, dispatch, getters}, {item_id, file}) {
            const servers = await dispatch('getHomeServers')
            const data = await servers.post(getters.signAuth, '/api/item_files/' + item_id + '/', file)
            if (data.name) {
                data.owner = state.user
                state.files.push(data)
                return data
            }
        },
        async deleteFile({state, dispatch, getters}, {id}) {
            const servers = await dispatch('getHomeServers')
            await servers.delete(getters.signAuth, '/api/files/' + id + '/')
            state.files = state.files.filter(file => file.id !== id)
        },
        async deleteItemFile({state, dispatch, getters}, {item_id, file_id}) {
            const servers = await dispatch('getHomeServers')
            await servers.delete(getters.signAuth, '/api/item_files/' + item_id + '/' + file_id + '/')
            state.files = state.files.filter(file => file.id !== file_id)
        },
        async stageFile({state, dispatch, getters}, {lifetime_id, file}) {
            const servers = await dispatch('getHomeServers')
            const data = await servers.post(getters.signAuth, '/api/staged_files/' + lifetime_id + '/', file)
            if (data.hash) {
                return data.hash
            }
        },
        async unstageFile({state, dispatch, getters}, {lifetime_id, file_hash}) {
            const servers = await dispatch('getHomeServers')
            await servers.delete(getters.signAuth, '/api/staged_files/' + lifetime_id + '/' + file_hash + '/')
        },
        async commitStagedFile({state, dispatch, getters}, {item_id, file_hash}) {
            const servers = await dispatch('getHomeServers')
            const data = await servers.post(getters.signAuth, '/api/item_files/' + item_id + '/', {file_hash})
            if (data.name) {
                data.owner = state.user
                state.files.push(data)
                return data
            }
        },
        async fetchTags({state, commit, dispatch, getters}) {
            if (state.last_load.tags > Date.now() - 1000 * 60 * 60 * 24) {
                return state.tags
            }
            const servers = await dispatch('getHomeServers')
            const data = await servers.get(getters.signAuth, '/api/tags/')
            commit('setTags', data)
            state.last_load.tags = Date.now()
            return data
        },
        async fetchProperties({state, commit, dispatch, getters}) {
            if (state.last_load.properties > Date.now() - 1000 * 60 * 60 * 24) {
                return state.properties
            }
            const servers = await dispatch('getHomeServers')
            const data = await servers.get(getters.signAuth, '/api/properties/')
            commit('setProperties', data)
            state.last_load.properties = Date.now()
            return data
        },
        async fetchCategories({state, commit, dispatch, getters}) {
            if (state.last_load.categories > Date.now() - 1000 * 60 * 60 * 24) {
                return state.categories
            }
            const servers = await dispatch('getHomeServers')
            const data = await servers.get(getters.signAuth, '/api/categories/')
            commit('setCategories', data)
            state.last_load.categories = Date.now()
            return data
        },
        async fetchAvailabilityPolicies({state, commit, dispatch, getters}) {
            if (state.last_load.availability_policies > Date.now() - 1000 * 60 * 60 * 24) {
                return state.availability_policies
            }
            const servers = await dispatch('getHomeServers')
            const data = await servers.get(getters.signAuth, '/api/availability_policies/')
            commit('setAvailabilityPolicies', data.map(policy => ({slug: policy[0], text: policy[1]})))
            state.last_load.availability_policies = Date.now()
            return data
        },
        async fetchStorageLocations({state, commit, dispatch, getters}) {
            if (state.last_load.storage_locations > Date.now() - 1000 * 60 * 60 * 24) {
                return state.storage_locations
            }
            const servers = await dispatch('getHomeServers')
            const data = await servers.get(getters.signAuth, '/api/storage_locations/')
            commit('setStorageLocations', data)
            state.last_load.storage_locations = Date.now()
            return data
        },
        async deleteStorageLocation({state, dispatch, getters}, location) {
            const servers = await dispatch('getHomeServers')
            const ret = await servers.delete(getters.signAuth, '/api/storage_locations/' + location.id + '/')
            dispatch('fetchStorageLocations')
            return ret
        },
        async createStorageLocation({state, dispatch, getters}, location) {
            const servers = await dispatch('getHomeServers')
            const data = {...location}
            if (data.parent === '') data.parent = null
            const reply = await servers.post(getters.signAuth, '/api/storage_locations/', data)
            state.last_load.storage_locations = 0
            return reply
        },
        async updateStorageLocation({state, dispatch, getters}, location) {
            const servers = await dispatch('getHomeServers')
            const data = {...location}
            if (data.parent === '') data.parent = null
            const reply = await servers.patch(getters.signAuth, '/api/storage_locations/' + location.id + '/', data)
            dispatch('fetchStorageLocations')
            return reply
        },
        async fetchInfo({state, commit, dispatch, getters}) {
            const last_load_info = Math.min(
                state.last_load.tags,
                state.last_load.properties,
                state.last_load.categories,
                state.last_load.availability_policies)
            if (last_load_info > Date.now() - 1000 * 60 * 60 * 24) {
                return state.info
            }
            const servers = await dispatch('getHomeServers')
            const data = await servers.get(getters.signAuth, '/api/info/')
            commit('setTags', data.tags)
            commit('setProperties', data.properties)
            commit('setCategories', data.categories)
            commit('setAvailabilityPolicies', data.availability_policies.map(policy => ({
                slug: policy[0],
                text: policy[1]
            })))
            commit('setDomains', data.domains)
            state.last_load.tags = Date.now()
            state.last_load.properties = Date.now()
            state.last_load.categories = Date.now()
            state.last_load.availability_policies = Date.now()
            return data
        },
        async userIdentityRecord({state}, {password}) {
            return await serializeIdentityRecord(state.user, state.keypair, password);
        },
        async fetchActiveWorkflows({state, commit, dispatch, getters}) {
            if (state.last_load.active_workflows > Date.now() - 1000 * 60 * 5) { // Cache for 5 minutes
                return state.active_workflows
            }
            const servers = await dispatch('getHomeServers')
            const data = await servers.get(getters.signAuth, '/api/workflows/')
            const workflows = data.map(deserializeWorkflowPayload)
            commit('setActiveWorkflows', workflows)
            state.last_load.active_workflows = Date.now()
            return workflows
        },
        async createWorkflow({state, commit, dispatch, getters}, workflowData) {
            const servers = await dispatch('getHomeServers')
            // The backend stores payload as an opaque string; the frontend (de)serializes it.
            // See docs/implementation.md#workflow-payload-is-an-opaque-string.
            const data = await servers.post(getters.signAuth, '/api/workflows/', serializeWorkflowPayload(workflowData))
            state.last_load.active_workflows = 0 // Invalidate cache
            return deserializeWorkflowPayload(data)
        },
        async updateWorkflow({state, commit, dispatch, getters}, workflow) {
            const servers = await dispatch('getHomeServers')
            const data = await servers.patch(getters.signAuth, '/api/workflows/' + workflow.id + '/', serializeWorkflowPayload(workflow))
            state.last_load.active_workflows = 0 // Invalidate cache
            return deserializeWorkflowPayload(data)
        },
        async updateWorkflowStep({state, commit, dispatch, getters}, {workflowId, currentStep, payload}) {
            const servers = await dispatch('getHomeServers')
            const data = await servers.post(getters.signAuth, '/api/workflows/' + workflowId + '/update_step/', {
                current_step: currentStep,
                payload: JSON.stringify(payload)
            })
            state.last_load.active_workflows = 0 // Invalidate cache
            return deserializeWorkflowPayload(data)
        },
        async deleteWorkflow({state, commit, dispatch, getters}, workflowId) {
            const servers = await dispatch('getHomeServers')
            await servers.delete(getters.signAuth, '/api/workflows/' + workflowId + '/')
            state.last_load.active_workflows = 0 // Invalidate cache
            return true
        }
    },
    getters: {
        isLoggedIn(state) {
            if (!state.local_loaded) {
                state.remember = localStorage.getItem('remember') === 'true'
                state.user = localStorage.getItem('user')
                state.token = localStorage.getItem('token')
                const keypair = localStorage.getItem('keypair')
                if (keypair)
                    state.keypair = nacl.crypto_sign_keypair_from_seed(nacl.from_hex(keypair))
                state.local_loaded = true
            }

            return state.user !== null && (state.token !== null || state.keypair !== null)
        },
        allUserProfiles(state) {
            // Combine user's own profile with all friend profiles
            const combined = {...state.friendProfiles};
            if (state.user && state.user_profile) {
                combined[state.user] = state.user_profile;
            }
            return combined;
        },
        signAuth(state) {
            return createSignAuth(state.user, state.keypair.signSk)
        },
        tokenAuth(state) {
            return createTokenAuth(state.token)
        },
        nullAuth(state) {
            return createNullAuth({})
        },
        inventory_items(state) {
            return state.item_map['/'] || []
        },
        groupInventoryItems: (state) => (groupId) => state.item_map['/group/' + groupId] || [],
        identityIdByHandle(state) {
            return Object.fromEntries(state.idmap.identities.map(i => [i.username, i.id]))
        },
        groupIdByHandle(state) {
            return Object.fromEntries(state.idmap.groups.map(g => [g.handle, g.id]))
        },
        // Reverse of the two getters above: turns a short-id's raw owner_identity_id/owner_group_id
        // back into a handle (see router.js EXPANDED_ROUTE_BUILDERS), no backend lookup needed.
        identityHandleById(state) {
            return Object.fromEntries(state.idmap.identities.map(i => [i.id, i.username]))
        },
        groupHandleById(state) {
            return Object.fromEntries(state.idmap.groups.map(g => [g.id, g.handle]))
        },
        loaded_items(state) {
            return Object.entries(state.item_map).reduce((acc, [url, items]) => {
                return acc.concat(items)
            }, [])
        },
        notifications(state) {
            // supported types: error, warning, info, login, success, friend
            const u = state.unreachable_neighbors.list().map(elem => {
                return {
                    type: 'error',
                    title: elem.domain + ' unreachable',
                    msg: 'The neighbor ' + elem.domain + ' is currently unreachable. Please try again later.',
                    time: elem.time
                }
            })
            return [...u, {
                type: 'info',
                title: 'Welcome to the Toolshed',
                msg: 'This is a federated social network. You can add friends from other servers and share items with them.',
                time: Date.now()
            }, {
                type: 'warning',
                title: 'Lorem ipsum',
                msg: 'Aliquam ex eros, imperdiet vulputate hendrerit et.',
                time: Date.now() - 1000 * 60 * 60 * 2
            }, {
                type: 'login',
                title: 'Login from 192.186.1.8',
                time: Date.now() - 1000 * 60 * 60 * 5
            }, {
                type: 'friend',
                title: 'New connection',
                msg: 'Christina accepted your request.',
                time: Date.now() - 1000 * 60 * 60 * 14
            }, {
                type: 'success',
                title: 'Lorem ipsum',
                msg: 'Aliquam ex eros, imperdiet vulputate hendrerit et.',
                time: Date.now() - 1000 * 60 * 60 * 24
            }]
        },
        getPreference: (state) => (key, fallbackDefault = null) => {
            if (Object.prototype.hasOwnProperty.call(state.devicePreferences, key) && state.devicePreferences[key] !== null) {
                return state.devicePreferences[key]
            }
            if (Object.prototype.hasOwnProperty.call(state.accountPreferences, key) && state.accountPreferences[key] !== null) {
                return state.accountPreferences[key]
            }
            const definition = state.preferenceDefinitions.find((pref) => pref.key === key)
            if (definition) {
                return definition.default
            }
            return fallbackDefault
        },
        /** Extracts the name from a handle like "git:tools#tag:drill"; returns non-handles unchanged. */
        getNameFromHandle: () => (handle) => {
            if (typeof handle !== 'string') {
                return handle;
            }
            const match = handle.match(/#(?:tag|property):(.+)$/);
            return match ? match[1] : handle;
        },
    }
})