import { computed, reactive } from 'vue';
import { api, ApiError, setCsrfToken } from './api';
import { activeFilters, boot, filterQuery } from './format';
import { useToasts } from './composables/useToasts';

/**
 * The one place the app's state lives — who is signed in, what they may do, the
 * sheet, its filters and the flags the UI needs while a request is in flight.
 * Small enough that a reactive object beats reaching for a store library.
 */

const EMPTY_FILTERS = { q: '', category_id: '', payment_method: '', from: '', to: '' };

const EMPTY_SUMMARY = {
    total: 0,
    count: 0,
    average: 0,
    largest: 0,
    by_category: [],
    by_method: [],
};

const toast = useToasts();

/** Filters survive a reload and stay shareable, so they start from the URL. */
function filtersFromUrl() {
    const params = new URLSearchParams(window.location.search);

    return Object.fromEntries(
        Object.keys(EMPTY_FILTERS).map((key) => [key, params.get(key) ?? ''])
    );
}

const state = reactive({
    // The shell already told us this, so the first paint is either the login
    // screen or the sheet — never a flash of the wrong one.
    user: boot.auth?.user ?? null,
    permissions: boot.auth?.permissions ?? [],
    loggingIn: false,

    filters: filtersFromUrl(),
    expenses: [],
    categories: [],
    summary: { ...EMPTY_SUMMARY },
    grandTotal: 0,

    loading: true,     // first load — the list area shows a skeleton
    refreshing: false, // a later load — the panel just dims
    saving: false,
    deletingId: null,
    failed: false,
});

/** Everything a response tells us about the current view. */
function absorb(data) {
    state.expenses = data.expenses;
    state.categories = data.categories;
    state.summary = data.summary;
    state.grandTotal = data.grand_total;
    state.failed = false;
}

function syncUrl() {
    const query = filterQuery(state.filters);

    window.history.replaceState({}, '', query ? `?${query}` : window.location.pathname);
}

/** Applies an /api/login or /api/logout answer. */
function absorbAuth(data) {
    state.user = data.auth.user;
    state.permissions = data.auth.permissions;
    setCsrfToken(data.csrf_token);
}

/** An expired session (401) drops us back to the login screen mid-action. */
function handleAuthLapse(error) {
    if (error instanceof ApiError && error.status === 401) {
        state.user = null;
        state.permissions = [];

        return true;
    }

    return false;
}

async function load({ silent = false } = {}) {
    if (silent) {
        state.refreshing = true;
    } else {
        state.loading = true;
    }

    try {
        absorb(await api.list(state.filters));
    } catch (error) {
        state.failed = ! handleAuthLapse(error);
        toast.error(error.message);
    } finally {
        state.loading = false;
        state.refreshing = false;
    }
}

export function useTracker() {
    const isFiltered = computed(() => Object.keys(activeFilters(state.filters)).length > 0);

    /** Gates the UI on the same permission names the server checks. */
    const can = (permission) => state.permissions.includes(permission);

    /**
     * Signs in and pulls the list straight away. Returns the field errors on a
     * 422 (wrong password included) so the form can mark them.
     */
    async function login(credentials) {
        state.loggingIn = true;

        try {
            const data = await api.login(credentials);

            absorbAuth(data);
            toast.success(data.message);
            await load();

            return { ok: true };
        } catch (error) {
            toast.error(error.message);

            return { ok: false, errors: error instanceof ApiError ? error.errors : {} };
        } finally {
            state.loggingIn = false;
        }
    }

    async function logout() {
        try {
            const data = await api.logout();

            absorbAuth(data);
            toast.info(data.message);
        } catch (error) {
            // Already signed out on the server — end up in the same place.
            if (! handleAuthLapse(error)) toast.error(error.message);
        }

        state.expenses = [];
        state.summary = { ...EMPTY_SUMMARY };
    }

    async function applyFilters(next) {
        Object.assign(state.filters, next);
        syncUrl();
        await load({ silent: true });
    }

    async function resetFilters() {
        Object.assign(state.filters, EMPTY_FILTERS);
        syncUrl();
        await load({ silent: true });
        toast.info('ফিল্টার রিসেট করা হয়েছে।');
    }

    /**
     * Create or update. Returns the field errors on a 422 so the form can mark
     * them inline; other failures are reported by toast and swallowed here.
     */
    async function save(payload, id = null) {
        state.saving = true;

        try {
            const data = id
                ? await api.update(id, payload, state.filters)
                : await api.create(payload, state.filters);

            absorb(data);
            toast.success(data.message);

            return { ok: true };
        } catch (error) {
            if (error instanceof ApiError && error.isValidation) {
                toast.error(error.message, { errors: error.errors });

                return { ok: false, errors: error.errors };
            }

            handleAuthLapse(error);
            toast.error(error.message);

            return { ok: false, errors: {} };
        } finally {
            state.saving = false;
        }
    }

    async function remove(expense) {
        state.deletingId = expense.id;

        try {
            const data = await api.remove(expense.id, state.filters);

            absorb(data);
            toast.success(data.message, { details: [expense.description] });

            return true;
        } catch (error) {
            handleAuthLapse(error);
            toast.error(error.message);

            // The row may be gone on the server — resync rather than lie about it.
            if (error instanceof ApiError && error.status === 404) {
                await load({ silent: true });
            }

            return false;
        } finally {
            state.deletingId = null;
        }
    }

    return {
        state,
        isFiltered,
        can,

        login,
        logout,
        load,
        applyFilters,
        resetFilters,
        save,
        remove,
    };
}
