/* The first run.

   What a phone that has just installed the app actually holds: a config
   file that does not exist yet, an empty database, no Soulseek account —
   and a library root that is NOT missing. `config.rs`'s `download_dir`
   falls back on Android to `app_data_dir()/Library`, a real, writable
   folder inside the app's own sandbox that nothing else on the phone can
   see, and it returns before the desktop's audio_dir() fallback is even
   reached. So the root is never null and "no folder yet" can never be a
   null check anywhere: the test is the PATH (isPrivateFolder), which
   You.svelte carried first and now imports from here, so there is one of
   it rather than two drifting apart.

   Two things the app genuinely cannot work without, and both are already
   fixable on a screen that exists: somewhere to keep music (You's folder
   picker and the all-files permission beside it) and, for anything that
   touches the network, a Soulseek sign-in (Community's card). This store
   knows which of the two are still missing, opens the sheet that says so,
   and stamps `setup_done` once the tour has run. It never handles a
   password: the Soulseek step navigates to the card and stops there. */
import * as api from "../lib/api.js";
import { openHalf, closeHalf } from "./nav.svelte.js";
import { slsk } from "./slsk.svelte.js";
import { lib, loadLibrary } from "./lib.svelte.js";
import { player } from "./player.svelte.js";

/** The sandbox root Android hands a fresh install —
 *  `/data/user/0/com.soulstream.app/files/Library`, or `/data/data/…` on an
 *  older device. A real folder, and an invisible one, which is why it reads
 *  as "no folder picked yet". A string match is all there is: change the
 *  package id and every phone silently reads as set up. */
export function isPrivateFolder(dir) {
    return !dir || /\/data\/(user\/\d+\/|data\/)com\.soulstream/.test(dir);
}

export const setup = $state({
    /** the config has been READ — not that it said anything in particular */
    loaded: false,
    /** "boot" = the tour opened itself · "preview" = You opened it ·
     *  "shown" = it has already stamped and must not stamp again */
    reason: "boot",
    folder: "",
    private: true,
    access: null,       // all-files access: true | false | null (never asked)
    done: false,        // the config's setup_done
    picking: false,
    /** stamps this store has written. A preview must leave it at zero — it
     *  is the whole proof that the door in You is not a door onto the
     *  config, and the phone walk asserts it on Kyle's own phone. */
    writes: 0,
});

/** Still the app's own sandbox folder. */
export function needsFolder() {
    return setup.loaded && setup.private;
}
/** Signed out, with no saved account to sign in with. Gated on the
 *  credentials having been read: before that every phone looks signed out,
 *  and a note that flashes on a set-up phone is a lie with good timing. */
export function needsAccount() {
    return slsk.credsRead && slsk.connection !== "online" && !slsk.savedUser;
}
/** What the walk reads back: "<folder>:<account>:<flag>". */
export function setupState() {
    return (setup.private ? "private" : "chosen")
        + ":" + (needsAccount() ? "out" : "in")
        + ":" + (setup.done ? "done" : "todo");
}

export async function loadSetup() {
    let cfg = null;
    try { cfg = await api.getSettings(); } catch { return; }
    setup.folder = cfg?.download_dir || "";
    setup.private = isPrivateFolder(cfg?.download_dir);
    setup.done = !!cfg?.setup_done;
    setup.loaded = true;
    await refreshAccess();
}

/** All-files access: true, false, or null where the phone never asked
 *  (an old device, or the desktop). A failed read leaves the last answer
 *  standing rather than turning the row back on. */
async function refreshAccess() {
    try { setup.access = (await api.allFilesAccess())?.granted ?? null; } catch { /* keep what we had */ }
}

/** Ask for it. The system answers by taking the app away and giving it
 *  back, so the grant is re-read on the way back — You.svelte watches
 *  visibility for exactly this reason, and without it the row stays on
 *  screen offering a permission that has already been given. */
export async function requestAccess() {
    try { await api.requestAllFilesAccess(); } catch (e) { player.error = String(e); return; }
    const back = () => {
        if (document.hidden) return;
        document.removeEventListener("visibilitychange", back);
        void refreshAccess();
    };
    document.addEventListener("visibilitychange", back);
}

/** The tour opened by hand — You's "Set up this phone again". A preview:
 *  it reads the real state and writes nothing, which is what makes it
 *  something the phone walk can drive on Kyle's own device. */
export function openSetup(reason = "preview") {
    setup.reason = reason;
    void loadSetup();
    openHalf("setup");
}

/** The boot gate: three clauses, ALL of them required.
 *
 *  The desktop gates on `setup_done` alone (App.svelte), and that alone
 *  would greet Kyle with a setup sheet on a phone full of his own music —
 *  the phone has never written the flag, and `config::setup_done` unwraps a
 *  missing field to false, so every phone in existence reads as unstamped.
 *  A saved account, or one record in the library, says this is not a fresh
 *  install whatever the flag says.
 *
 *  Both of those are read from the backend, so the caller has to let
 *  `loadLibrary()` and `connectStored()` settle first: asked early they are
 *  `[]` and `""` on every phone there is, and the gate would open on all of
 *  them. */
export async function maybeOpenFirstRun() {
    if (!setup.loaded) await loadSetup();
    if (!setup.loaded) return false;
    if (setup.done || slsk.savedUser || lib.albums.length) return false;
    setup.reason = "boot";
    openHalf("setup");
    return true;
}

/** The library folder, picked from the tour — You's own three calls: pick,
 *  save, rescan. Keeping the private folder is a legitimate answer, so
 *  nothing here is required and a cancelled picker is not an error. */
export async function chooseFolder() {
    if (setup.picking) return;
    setup.picking = true;
    try {
        const r = await api.pickLibraryFolder();
        if (!r?.path) { player.error = "That folder has no path the phone can scan — pick one on this phone's storage."; return; }
        await api.setSettings({ downloadDir: r.path });
        await loadSetup();
        await api.rescanLibrary();
        await loadLibrary();
    } catch (e) {
        if (!/cancelled/.test(String(e))) player.error = String(e);
    } finally {
        setup.picking = false;
    }
}

/** Close the tour and remember that it ran — whichever door closed it: the
 *  foot's button, the scrim, the back gesture, or a row that navigated away
 *  (a tour that ran is a tour that ran). `reason` moves to "shown" BEFORE
 *  the await, so two doors closing in the same tick still stamp once.
 *
 *  A preview writes nothing at all. The write is best effort otherwise: a
 *  failed save leaves the flag false and the tour comes back on the next
 *  launch, which is the honest outcome — nothing else depends on it. */
export async function finishSetup() {
    closeHalf();
    if (setup.reason !== "boot") return;
    setup.reason = "shown";
    try {
        await api.setSettings({ setupDone: true });
        setup.done = true;
        setup.writes++;
    } catch { /* see above */ }
}
