// Samba provisioning for the brand Pi filesystem.
//
// Same shape as apt-resolve.ts: pure decision functions in this file, no I/O;
// the installer wraps them with the side-effecting spawnSync + log lines. The
// pure layer is fully unit-tested; the wrapper is exercised end-to-end on a
// real Pi during install/uninstall verification.
//
// What this module owns:
//   1. Pick the LAN interface to bind smbd to (loopback excluded).
//   2. Render the brand-scoped Samba stanza per the spec in the task brief.
//   3. Merge that stanza into an existing /etc/samba/smb.conf idempotently —
//      replace if a stanza for this brand already exists, otherwise append.
//   4. Remove this brand's stanza on uninstall while leaving every peer brand's
//      stanza intact (peer-brand isolation is the structural invariant).
//   5. Report whether any brand stanza remains so the uninstall step can
//      decide whether to apt-purge samba.
// ---------------------------------------------------------------------------
// Brand-scoped Samba stanza
// ---------------------------------------------------------------------------
/**
 * Render the `[<brand>]` share stanza. Exact directives are the task's spec
 * verbatim: share rooted at `sharePath`, writable by `installOwner` only,
 * force-uid to `installOwner` so files created via SMB are owned by the same
 * uid as files created over SSH, and standard create/dir masks for an ext4
 * home directory. `installOwner` is the Unix user that owns this brand's
 * install on the device — `admin` on a Pi or Hetzner box, the operator's
 * Unix user on a self-hosted laptop. Task 534.
 */
export function renderBrandStanza(input) {
    return [
        `[${input.brand}]`,
        `   path = ${input.sharePath}`,
        `   read only = no`,
        `   valid users = ${input.installOwner}`,
        `   force user = ${input.installOwner}`,
        `   browseable = yes`,
        `   create mask = 0664`,
        `   directory mask = 0775`,
        ``,
    ].join("\n");
}
/**
 * Render the `[global]` section. `interfaces = lo <lan>` plus `bind interfaces
 * only = yes` is the LAN-only posture: smbd accepts connections on the LAN
 * interface and loopback, nothing else. Cloudflare tunnels carry HTTPS only,
 * so this is the structural guarantee that SMB never leaves the LAN even if
 * the firewall is misconfigured upstream.
 */
export function renderGlobalSection(input) {
    return [
        `[global]`,
        `   workgroup = WORKGROUP`,
        `   server string = %h Samba`,
        `   server role = standalone server`,
        `   interfaces = lo ${input.lanInterface}`,
        `   bind interfaces only = yes`,
        `   log file = /var/log/samba/log.%m`,
        `   max log size = 1000`,
        `   panic action = /usr/share/samba/panic-action %d`,
        `   passdb backend = tdbsam`,
        `   unix password sync = no`,
        `   map to guest = bad user`,
        `   usershare allow guests = no`,
        ``,
    ].join("\n");
}
/**
 * Build a complete smb.conf from scratch — globals + one brand stanza. Used
 * only when no existing config is present (fresh apt install always writes
 * one, so this branch fires on weird edge cases like operator-deleted conf
 * files). Normal path is `mergeSmbConf` against the apt-shipped default.
 */
export function renderFullSmbConf(input) {
    return renderGlobalSection({ lanInterface: input.lanInterface }) +
        renderBrandStanza({ brand: input.brand, sharePath: input.sharePath, installOwner: input.installOwner });
}
// ---------------------------------------------------------------------------
// LAN interface detection
// ---------------------------------------------------------------------------
/**
 * Pick the LAN interface to bind smbd to. Preference order: wlan0, eth0, then
 * the first non-loopback interface with a non-internal IPv4 address. Returns
 * null when no such interface exists — the caller treats that as a hard
 * failure (the Pi has no LAN connectivity; SMB has nothing to bind to).
 *
 * Input is the shape returned by `os.networkInterfaces()` so the test can pass
 * realistic fixtures without spinning up real interfaces.
 */
export function pickLanInterface(ifaces) {
    const hasIPv4 = (name) => {
        const addrs = ifaces[name];
        if (!addrs)
            return false;
        return addrs.some((a) => a.family === "IPv4" && !a.internal);
    };
    if (hasIPv4("wlan0"))
        return "wlan0";
    if (hasIPv4("eth0"))
        return "eth0";
    for (const name of Object.keys(ifaces)) {
        if (name === "lo")
            continue;
        if (hasIPv4(name))
            return name;
    }
    return null;
}
// ---------------------------------------------------------------------------
// smb.conf merge / remove
// ---------------------------------------------------------------------------
/**
 * Find the start and end byte offsets of a `[<section>]` block in an smb.conf.
 * End is the index just before the next `[…]` header or EOF, whichever comes
 * first. Returns null when no such section exists.
 *
 * Section names are matched case-sensitively because Samba itself is
 * case-sensitive for share names on Linux filesystems.
 */
function findSectionRange(conf, section) {
    const lines = conf.split("\n");
    const header = `[${section}]`;
    let startLine = -1;
    for (let i = 0; i < lines.length; i++) {
        if (lines[i].trim() === header) {
            startLine = i;
            break;
        }
    }
    if (startLine === -1)
        return null;
    let endLine = lines.length;
    for (let i = startLine + 1; i < lines.length; i++) {
        if (/^\[[^\]]+\]\s*$/.test(lines[i].trim())) {
            endLine = i;
            break;
        }
    }
    const start = lines.slice(0, startLine).reduce((n, l) => n + l.length + 1, 0);
    const sectionText = lines.slice(startLine, endLine).join("\n") + (endLine < lines.length ? "\n" : "");
    return { start, end: start + sectionText.length };
}
/**
 * Merge globals + brand stanza into an existing smb.conf. Idempotent: a
 * second call with the same inputs produces byte-identical output.
 *
 *   - `[global]`: replace verbatim with our rendered globals. The apt-shipped
 *     `[global]` is a good starting point but doesn't carry our LAN-only
 *     directives; replacing it is the only way to guarantee `bind interfaces
 *     only = yes` is in effect.
 *   - `[<brand>]`: replace if present, append at end of file otherwise. Peer
 *     brand stanzas (other `[…]` blocks) are preserved verbatim.
 */
export function mergeSmbConf(input) {
    const { existing, brand, sharePath, lanInterface, installOwner } = input;
    const newGlobals = renderGlobalSection({ lanInterface });
    const newBrand = renderBrandStanza({ brand, sharePath, installOwner });
    let conf = existing;
    // Replace or insert the global section.
    const globalRange = findSectionRange(conf, "global");
    if (globalRange) {
        conf = conf.slice(0, globalRange.start) + newGlobals + conf.slice(globalRange.end);
    }
    else {
        conf = newGlobals + (conf.startsWith("\n") ? conf : conf);
    }
    // Replace or insert the brand stanza.
    const brandRange = findSectionRange(conf, brand);
    if (brandRange) {
        conf = conf.slice(0, brandRange.start) + newBrand + conf.slice(brandRange.end);
    }
    else {
        // Append with a single trailing newline guarantee.
        if (!conf.endsWith("\n"))
            conf += "\n";
        conf += newBrand;
    }
    return conf;
}
/**
 * Remove `[<brand>]` from an smb.conf. Returns the input unchanged when no
 * such stanza exists. Other sections (global, other brands) are preserved
 * byte-for-byte. Used by the uninstall path.
 */
export function removeBrandStanza(input) {
    const { existing, brand } = input;
    const range = findSectionRange(existing, brand);
    if (!range)
        return existing;
    return existing.slice(0, range.start) + existing.slice(range.end);
}
/**
 * True when at least one non-global stanza remains in the smb.conf. Used by
 * uninstall to decide whether to apt-purge samba: only purge when no brand
 * stanza is left.
 */
export function hasAnyBrandStanza(conf) {
    const lines = conf.split("\n");
    for (const line of lines) {
        const m = line.trim().match(/^\[([^\]]+)\]$/);
        if (m && m[1].toLowerCase() !== "global")
            return true;
    }
    return false;
}
// ---------------------------------------------------------------------------
// Step-marker contract
// ---------------------------------------------------------------------------
/**
 * The four install-invariant markers the installer (and uninstall, set-pin)
 * emit. Locked here so a typo at a call site cannot silently drift from the
 * spec. The state vocabulary is also locked: `ok` (step succeeded), `fail:
 * <stderr>` (step threw — installer aborts), `deferred reason=<…>` (step
 * intentionally skipped because a precondition wasn't met).
 */
export const SAMBA_STEPS = ["apt", "conf", "user", "units"];
export function formatSambaMarker(step, state) {
    return `[install-invariant] samba-provision-${step} ${state}`;
}
