/**
 * Author's Voice plugin for OpenWriter.
 * Proxies /api/voice/* to the AV backend and adds editor context-menu items
 * for sub-paragraph text actions (Enhance / Modify / Shrink / Expand / Insert / Fill).
 *
 * Sidebar document transforms (Vary / Shrinkify / Threadify / etc.) live in
 * @openwriter/plugin-publish — they go through the metered platform path.
 */
import { readFileSync } from 'fs';
import { homedir } from 'os';
import { join } from 'path';
const PLUGIN_NAME = '@openwriter/plugin-authors-voice';
/**
 * Live-read the model tier from ~/.openwriter/config.json per request.
 * The host passes plugin config ONCE at registerRoutes and never reloads it on save, so the
 * dropdown's value would otherwise require a server restart. Reading the file per request
 * makes a model-picker change take effect on the very next Enhance. Falls back to the
 * load-time value on any error.
 */
function liveModelTier(fallback) {
    try {
        const cfg = JSON.parse(readFileSync(join(homedir(), '.openwriter', 'config.json'), 'utf8'));
        const v = cfg?.plugins?.[PLUGIN_NAME]?.config?.model;
        return typeof v === 'string' ? v : fallback;
    }
    catch {
        return fallback;
    }
}
/** Strip a leading YAML frontmatter block so it never lands in the voice corpus. */
function stripFrontmatter(md) {
    return md.replace(/^﻿?---\r?\n[\s\S]*?\r?\n---\r?\n?/, '').trim();
}
/** Pull OpenWriter's stable doc id out of the on-disk markdown (frontmatter carries
 *  `"docId":"..."`). Returns null when absent so the caller can fall back to the filename. */
function extractOwDocId(md) {
    const m = typeof md === 'string' ? md.match(/"docId"\s*:\s*"([^"]+)"/) : null;
    return m ? m[1] : null;
}
const plugin = {
    name: '@openwriter/plugin-authors-voice',
    version: '0.4.0',
    description: "Rewrite text in your voice using Author's Voice",
    category: 'writing',
    configSchema: {
        'api-key': {
            type: 'string',
            required: true,
            env: 'AV_API_KEY',
            description: 'Author\'s Voice API key',
        },
        'backend-url': {
            type: 'string',
            env: 'AV_BACKEND_URL',
            description: 'AV backend URL',
        },
        // Model tier for rewrites. Sent as `modelTier` on /api/voice/* (the AV API owns the
        // tier→model map + default — this is a pure pass-through of the user's pick). Blank =
        // API default (strongest). Labels mirror the API's TIER_PICKER_OPTIONS.
        'model': {
            type: 'select',
            env: 'AV_MODEL_TIER',
            description: 'Writing model',
            options: [
                { value: '', label: 'Default (Strongest)' },
                { value: 'strongest', label: 'Strongest — Claude Opus (best quality, $$$)' },
                { value: 'balanced', label: 'Balanced — Claude Sonnet ($$)' },
                { value: 'fast-plus', label: 'Fast+ — Gemini 3.5 Flash (newest, great, $)' },
                { value: 'fast', label: 'Fast — Gemini 2.5 Flash (free)' },
            ],
        },
    },
    registerRoutes(ctx) {
        const backendUrl = ctx.config['backend-url'] || process.env.AV_BACKEND_URL || 'https://authors-voice.com';
        const apiKey = ctx.config['api-key'] || process.env.AV_API_KEY || '';
        const debugEnabled = process.env.AV_DEBUG === '1' || process.env.AV_DEBUG === 'true';
        const modelTier = ctx.config['model'] || process.env.AV_MODEL_TIER || '';
        const authHeaders = () => {
            const h = { 'Content-Type': 'application/json' };
            if (apiKey)
                h['Authorization'] = `Bearer ${apiKey}`;
            return h;
        };
        const withDebug = (body) => {
            if (!debugEnabled || !body || typeof body !== 'object')
                return body;
            return { ...body, debug: true };
        };
        // Inject the user's model-tier pick, read LIVE per request (dropdown changes take effect
        // immediately, no restart). Pure pass-through: the AV API validates the slug and falls
        // back to its own default if absent/unknown. Blank → nothing injected.
        const withModel = (body) => {
            const tier = liveModelTier(modelTier);
            if (!tier || !body || typeof body !== 'object')
                return body;
            return { ...body, modelTier: tier };
        };
        // Sidebar ingestion: right-click a doc (or a whole workspace/container) → "Add to
        // Author's Voice". OpenWriter pre-fetches the doc's on-disk markdown and hands it here
        // as `content`, so no file-upload primitive is needed — the doc IS the sample. We strip
        // frontmatter, key the corpus docId off the OW doc's stable id (idempotent re-sync:
        // re-running replaces that doc's chunks), and post to the AV corpus endpoint. Registered
        // BEFORE the /api/voice/* wildcard so it isn't proxied straight through to the backend.
        ctx.app.post('/api/voice/sidebar-action', async (req, res) => {
            try {
                const { filename, title, content } = req.body || {};
                const body = stripFrontmatter(typeof content === 'string' ? content : '');
                if (!body.trim()) {
                    res.status(400).json({ error: 'Document is empty — nothing to add to your voice.' });
                    return;
                }
                // Stable, namespaced corpus id so re-adding the same doc replaces its prior chunks.
                const owId = extractOwDocId(content) || String(filename || '').replace(/\.[^.]+$/, '');
                const docId = `ow-${owId}`;
                const upstream = await fetch(`${backendUrl}/api/voice/content`, {
                    method: 'POST',
                    headers: authHeaders(),
                    body: JSON.stringify({ docId, content: body, categories: ['openwriter'], authenticity: 'Human' }),
                });
                const text = await upstream.text();
                let data;
                try {
                    data = JSON.parse(text);
                }
                catch {
                    console.error('[AV Plugin] Non-JSON response (sidebar-action):', text.substring(0, 500));
                    res.status(502).json({ error: 'AV backend returned non-JSON response' });
                    return;
                }
                if (!upstream.ok) {
                    res.status(upstream.status).json(data);
                    return;
                }
                res.json({
                    success: true,
                    action: 'add-to-corpus',
                    title: title || docId,
                    docId,
                    chunksCreated: data?.chunksCreated ?? 0,
                });
            }
            catch (err) {
                console.error('[AV Plugin] sidebar-action error:', err?.message || err);
                res.status(502).json({ error: 'AV backend unreachable' });
            }
        });
        // Wildcard proxy for /api/voice/* routes. Pure pass-through: the AV API owns the
        // engine choice (v1/v2) via its own AV_DEFAULT_ENGINE setting, so the plugin injects
        // nothing but the optional owner-only dev debug flag.
        ctx.app.post('/api/voice/*', async (req, res) => {
            try {
                const subPath = req.params[0] || '';
                const targetUrl = `${backendUrl}/api/voice/${subPath}`;
                const body = withModel(withDebug(req.body));
                console.log(`[AV Plugin] ${req.method} ${req.path} → ${targetUrl}`);
                const upstream = await fetch(targetUrl, {
                    method: 'POST',
                    headers: authHeaders(),
                    body: JSON.stringify(body),
                });
                res.status(upstream.status);
                const forwardHeaders = ['x-usage-rewrite-count', 'x-usage-rewrite-limit', 'x-usage-resets-at'];
                for (const h of forwardHeaders) {
                    const val = upstream.headers.get(h);
                    if (val)
                        res.setHeader(h, val);
                }
                const responseText = await upstream.text();
                try {
                    const data = JSON.parse(responseText);
                    res.json(data);
                }
                catch {
                    console.error('[AV Plugin] Non-JSON response:', responseText.substring(0, 500));
                    res.status(502).json({ error: 'AV backend returned non-JSON response' });
                }
            }
            catch (err) {
                console.error('[AV Plugin] Backend error:', err?.message || err);
                res.status(502).json({ error: 'AV backend unreachable' });
            }
        });
        // Wildcard GET proxy for /api/voice/* — same key-injection + base-URL pattern as the POST
        // proxy above, no body. Covers the wallet/top-up reads (GET /api/voice/billing and
        // /api/voice/billing/topup-options); the matching POST /api/voice/billing/topup rides the
        // POST wildcard. Query string is forwarded so any future paginated read just works.
        ctx.app.get('/api/voice/*', async (req, res) => {
            try {
                const subPath = req.params[0] || '';
                const qs = req.originalUrl.includes('?') ? req.originalUrl.slice(req.originalUrl.indexOf('?')) : '';
                const targetUrl = `${backendUrl}/api/voice/${subPath}${qs}`;
                console.log(`[AV Plugin] ${req.method} ${req.path} → ${targetUrl}`);
                const upstream = await fetch(targetUrl, {
                    method: 'GET',
                    headers: authHeaders(),
                });
                res.status(upstream.status);
                const responseText = await upstream.text();
                try {
                    res.json(JSON.parse(responseText));
                }
                catch {
                    console.error('[AV Plugin] Non-JSON response:', responseText.substring(0, 500));
                    res.status(502).json({ error: 'AV backend returned non-JSON response' });
                }
            }
            catch (err) {
                console.error('[AV Plugin] Backend error:', err?.message || err);
                res.status(502).json({ error: 'AV backend unreachable' });
            }
        });
    },
    contextMenuItems() {
        return [
            // Selection actions (require highlighted text)
            { label: 'Enhance', shortcut: 'R', action: 'av:rewrite', condition: 'has-selection' },
            { label: 'Modify...', action: 'av:custom', condition: 'has-selection', promptForInput: true },
            { label: 'Shrink', shortcut: 'S', action: 'av:shrink', condition: 'has-selection' },
            { label: 'Expand', shortcut: 'E', action: 'av:expand', condition: 'has-selection' },
            // Empty node actions (cursor on empty line)
            { label: 'Insert', shortcut: 'I', action: 'av:insert', condition: 'empty-node', promptForInput: true },
            { label: 'Fill paragraph', shortcut: 'F', action: 'av:fill', condition: 'empty-node' },
            { label: 'Fill sentence', action: 'av:fill-sentence', condition: 'empty-node' },
        ];
    },
    // Sidebar (file tree) right-click. `voice:` prefix routes the dispatch to this plugin's
    // POST /api/voice/sidebar-action. folderCapable → also offered on workspaces/containers,
    // where OpenWriter applies it to every doc in the folder.
    sidebarMenuItems() {
        return [
            { label: "Add to Author's Voice", action: 'voice:add-to-corpus', folderCapable: true },
        ];
    },
};
export default plugin;
