// Setup / multi-project config for the social-autoposter MCP.
//
// Source of truth = config.json projects[] (the pipeline reads it). Readiness is
// DERIVED per-project from whether its required fields are present, never stored
// as a boolean (so the saved config and the reported status can't disagree).
//
// The only thing persisted outside config.json is a small scoping list: which
// project names were set up via THIS install. That list exists so (a) multi-
// project disambiguation works and (b) we don't surface unrelated projects that
// happen to live in config.json. It is NOT the source of truth for readiness.
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { REPO_DIR } from "./repo.js";
// Per-install scoping list lives outside the repo so it survives repo updates.
const STATE_DIR = process.env.SAPS_STATE_DIR || path.join(os.homedir(), ".social-autoposter-mcp");
const STATE_PATH = path.join(STATE_DIR, "setup-state.json");
// The pipeline reads projects[] from config.json. Override for tests / custom
// installs; defaults to the repo's config.json.
export const CONFIG_PATH = process.env.SAPS_CONFIG_PATH || path.join(REPO_DIR, "config.json");
// Fields the X drafting prompts genuinely consume. Required ones must all be
// present before a project is "ready"; recommended ones improve draft quality.
export const REQUIRED_FIELDS = [
    "name",
    "website",
    "description",
    "icp",
    "voice",
    // search_topics is required: the cycle's topic picker reads the DB universe
    // (project_search_topics) seeded FROM these on setup. With zero topics the
    // picker raises and the whole draft cycle silently returns nothing, so a
    // project is NOT ready until it has at least one topic to seed. (2026-06-02)
    "search_topics",
];
export const RECOMMENDED_FIELDS = [
    "differentiator",
    "get_started_link",
    "content_guardrails",
];
function readScope() {
    try {
        if (!fs.existsSync(STATE_PATH))
            return { projects: [] };
        const s = JSON.parse(fs.readFileSync(STATE_PATH, "utf-8"));
        // New shape.
        if (Array.isArray(s.projects)) {
            return { projects: s.projects, last_project: s.last_project };
        }
        // Migrate old single-project shape { configured, project }.
        if (typeof s.project === "string") {
            return { projects: [s.project], last_project: s.project };
        }
        return { projects: [] };
    }
    catch {
        return { projects: [] };
    }
}
function writeScope(s) {
    fs.mkdirSync(STATE_DIR, { recursive: true });
    fs.writeFileSync(STATE_PATH, JSON.stringify(s, null, 2) + "\n", "utf-8");
}
export function managedProjects() {
    return readScope().projects;
}
export function recordManagedProject(name) {
    const s = readScope();
    if (!s.projects.includes(name))
        s.projects.push(name);
    s.last_project = name;
    writeScope(s);
}
function readConfig() {
    if (!fs.existsSync(CONFIG_PATH))
        return { projects: [] };
    const raw = fs.readFileSync(CONFIG_PATH, "utf-8").trim();
    if (!raw)
        return { projects: [] };
    return JSON.parse(raw);
}
export function projectExists(name) {
    try {
        return (readConfig().projects || []).some((p) => p.name === name);
    }
    catch {
        return false;
    }
}
function normalizeTopics(t) {
    if (t == null)
        return undefined;
    if (Array.isArray(t))
        return t.map((x) => String(x).trim()).filter(Boolean);
    return String(t)
        .split(/[,\n]/)
        .map((x) => x.trim())
        .filter(Boolean);
}
// The fields the user actually supplies via setup. Only fields that are present
// AND non-empty get written, so an incremental call merges just what it carries
// and never blanks an existing field or clobbers weight/platform/links/github.
// name is always included (it's the match key).
function userFields(input) {
    const fields = {};
    const setStr = (k) => {
        const v = input[k];
        if (v != null && String(v).trim() !== "")
            fields[k] = v;
    };
    if (input.name != null)
        fields.name = input.name;
    setStr("website");
    setStr("description");
    setStr("icp");
    setStr("voice");
    setStr("differentiator");
    const topics = normalizeTopics(input.search_topics);
    if (topics && topics.length)
        fields.search_topics = topics;
    setStr("get_started_link");
    setStr("content_guardrails");
    return fields;
}
// A brand-new project entry: user fields plus the defaults a fresh X-rail
// project needs. Applied ONLY on create, never on update.
export function buildProjectEntry(input) {
    return {
        weight: 10,
        platform: "twitter",
        voice_relationship: "first_party",
        ...userFields(input),
    };
}
// Upsert the project into config.json projects[] (match by name), incrementally
// merging only the supplied fields, and record it as managed by this install.
// Backs up config.json first. Does NOT require all fields — readiness is checked
// separately, so a project can be filled out over several calls.
export function applySetup(input) {
    const cfg = readConfig();
    cfg.projects = cfg.projects || [];
    const idx = cfg.projects.findIndex((p) => p.name === input.name);
    let created;
    if (idx >= 0) {
        // Update: merge ONLY supplied fields; keep every other existing field.
        cfg.projects[idx] = { ...cfg.projects[idx], ...userFields(input) };
        created = false;
    }
    else {
        cfg.projects.push(buildProjectEntry(input));
        created = true;
    }
    if (fs.existsSync(CONFIG_PATH)) {
        const stamp = new Date().toISOString().replace(/[:.]/g, "-");
        fs.copyFileSync(CONFIG_PATH, `${CONFIG_PATH}.bak-${stamp}`);
    }
    fs.mkdirSync(path.dirname(CONFIG_PATH), { recursive: true });
    fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
    recordManagedProject(input.name);
    const missing = missingForProject(input.name) ?? [];
    return { project: input.name, created, ready: missing.length === 0, missing_required: missing };
}
// ---------------------------------------------------------------------------
// Readiness (derived from config.json, never stored).
// ---------------------------------------------------------------------------
// Which required fields are missing on the persisted project in config.json.
// Returns null when the named project can't be found/read.
export function missingForProject(name) {
    if (!name)
        return null;
    try {
        const proj = (readConfig().projects || []).find((p) => p.name === name);
        if (!proj)
            return null;
        return REQUIRED_FIELDS.filter((f) => {
            const v = proj[f];
            if (v == null)
                return true;
            if (typeof v === "string")
                return v.trim() === "";
            if (Array.isArray(v))
                return v.length === 0;
            if (typeof v === "object")
                return Object.keys(v).length === 0;
            return false;
        });
    }
    catch {
        return null;
    }
}
export function projectStatus(name) {
    const missing = missingForProject(name);
    if (missing === null) {
        return { name, in_config: false, ready: false, missing_required: [...REQUIRED_FIELDS] };
    }
    return { name, in_config: true, ready: missing.length === 0, missing_required: missing };
}
// Status of every project this install manages.
export function listManagedProjectStatus() {
    return managedProjects().map(projectStatus);
}
export function hasReadyProject() {
    return listManagedProjectStatus().some((s) => s.ready);
}
const SETUP_REQUIRED_MESSAGE = "No project is set up yet. Run the `setup` tool first: collect from the user their website " +
    "URL, what the product does (description), who to target (icp), and brand voice/tone, then " +
    "call setup with a short name plus those fields. You can set up multiple products; each is " +
    "configured independently and you fill the fields incrementally.";
export function resolveProject(requested) {
    if (requested) {
        const st = projectStatus(requested);
        if (!st.in_config) {
            return {
                ok: false,
                message: `Project '${requested}' isn't set up yet. Run setup with name='${requested}' plus its ` +
                    `website, description, icp, and voice.`,
            };
        }
        if (!st.ready) {
            return {
                ok: false,
                message: `Project '${requested}' still needs: ${st.missing_required.join(", ")}. Ask the user ` +
                    `for those and call setup again with name='${requested}'.`,
            };
        }
        return { ok: true, project: requested };
    }
    const statuses = listManagedProjectStatus();
    const ready = statuses.filter((s) => s.ready).map((s) => s.name);
    if (ready.length === 1)
        return { ok: true, project: ready[0] };
    if (ready.length > 1) {
        return {
            ok: false,
            message: `Multiple projects are set up (${ready.join(", ")}). Tell me which one to use (pass the project name).`,
        };
    }
    const partial = statuses.filter((s) => !s.ready).map((s) => s.name);
    if (partial.length) {
        return { ok: false, message: `No project is fully set up yet. Finish setup for: ${partial.join(", ")}.` };
    }
    return { ok: false, message: SETUP_REQUIRED_MESSAGE };
}
