// The delete path for `agents artifacts share delete` / `agents artifacts unshare` — an authed DELETE
// to the Worker, which already implements it (worker-template.ts). Mirrors
// publish.ts: pure target-resolution logic is exported for tests, the network
// calls (a status check + a delete) sit behind an injectable DI seam.
//
// The Worker's R2 delete is idempotent — DELETE on a key that never existed still
// returns `{"ok":true}` — so `{"ok":true}` alone is never proof of a takedown.
// Every delete here is followed by a status check that must observe 404 before
// the operation is reported as successful.
import { resolveShareBackend } from './backend.js';
import { buildShareKey, resolveShareUsername } from './publish.js';
import { extractShareHttpError, formatShareHttpErrorDetail } from './http-error.js';
/**
 * Normalize any of the three accepted target forms to the R2 key that publish
 * would have written:
 *   - a full share URL: `https://share.getrush.ai/<user>/<slug>`
 *   - `<user>/<slug>`
 *   - a bare `<slug>` — resolved against the caller's own namespace exactly as
 *     `publishToEndpoint` resolves it at publish time (resolveShareUsername +
 *     buildShareKey), so a bare slug always targets *your* published page.
 *
 * The URL and `<user>/<slug>` forms are taken as already the exact key a prior
 * publish produced (no re-sanitizing) — only the bare-slug form runs through
 * `buildShareKey`, because that's the one case where the slug hasn't already
 * been normalized by a publish.
 */
export async function resolveDeleteTarget(target, opts = {}) {
    const trimmed = target.trim();
    if (!trimmed)
        throw new Error('Share target is empty.');
    let key;
    if (/^https?:\/\//i.test(trimmed)) {
        const url = new URL(trimmed);
        const segments = url.pathname
            .replace(/^\/+|\/+$/g, '')
            .split('/')
            .filter(Boolean)
            .map(decodeURIComponent);
        if (segments.length < 2) {
            throw new Error(`Not a share page URL (expected .../<user>/<slug>): ${trimmed}`);
        }
        key = segments.slice(0, 2).join('/');
    }
    else if (trimmed.includes('/')) {
        const segments = trimmed.replace(/^\/+|\/+$/g, '').split('/').filter(Boolean);
        if (segments.length !== 2) {
            throw new Error(`Expected <user>/<slug>, got: ${trimmed}`);
        }
        key = segments.join('/');
    }
    else {
        const username = await resolveShareUsername(opts);
        key = buildShareKey(username, trimmed);
    }
    return { key, coverKey: `${key}.png` };
}
async function defaultCheck(url) {
    const res = await fetch(url, { method: 'HEAD' });
    return { status: res.status };
}
async function defaultDelete(url, headers) {
    const res = await fetch(url, { method: 'DELETE', headers });
    if (res.ok)
        return { ok: true, status: res.status };
    const body = await res.text().catch(() => undefined);
    return { ok: false, status: res.status, body, retryAfter: res.headers.get('retry-after') ?? undefined };
}
async function defaultRevisionsFetch(url) {
    const res = await fetch(url, { headers: { accept: 'application/json' } });
    return { status: res.status, contentType: res.headers.get('content-type') ?? '', body: await res.text() };
}
/**
 * List the R2 keys of a slug's retained revisions via the Worker's
 * `?revisions=json` route (worker-template.ts `renderRevisions`). Best-effort:
 * any failure to reach or parse the route (an endpoint provisioned before
 * revisions existed, a network blip) yields an empty list rather than
 * throwing — purging revisions is a safety improvement on top of the primary
 * delete contract (take the canonical page down), never a reason to fail it.
 */
async function listRevisionKeys(endpoint, key, fetchRevisions) {
    try {
        const url = `${endpoint.baseUrl.replace(/\/+$/, '')}/${key}?revisions=json`;
        const res = await fetchRevisions(url);
        if (res.status !== 200 || !/application\/json/i.test(res.contentType))
            return [];
        const data = JSON.parse(res.body);
        if (!Array.isArray(data.revisions))
            return [];
        return data.revisions.map((r) => String(r.key ?? '')).filter(Boolean);
    }
    catch {
        return [];
    }
}
/**
 * Delete one R2 object behind the share Worker and assert the postcondition.
 * `{"ok":true}` from the Worker is not evidence a page came down — R2 delete is
 * idempotent, so a DELETE on a key that was never there also returns `ok:true`.
 * This checks existence before (so callers can tell "deleted" from "was never
 * there") and re-checks after (so callers can tell "deleted" from "still public").
 */
export async function deleteObject(endpoint, key, opts = {}) {
    const url = `${endpoint.baseUrl.replace(/\/+$/, '')}/${key}`;
    const check = opts.checker ?? defaultCheck;
    const del = opts.deleter ?? defaultDelete;
    const before = await check(url);
    const existedBefore = before.status !== 404;
    const r = await del(url, { authorization: `Bearer ${endpoint.token}` });
    if (!r.ok) {
        const detail = formatShareHttpErrorDetail(extractShareHttpError({ status: r.status, body: r.body, retryAfter: r.retryAfter }));
        throw new Error(`Delete failed (${r.status}) for ${url}${detail}. Check the bearer (Phoenix session or WRITE_TOKEN), or that 'agents artifacts setup' completed.`);
    }
    const after = await check(url);
    const verified404 = after.status === 404;
    return { key, url, existedBefore, deleted: r.ok, verified404 };
}
/** Delete one share target (page + by default its OG cover) and verify both
 * are gone. Throws on an unverified takedown — never reports success for an
 * object that still resolves. */
export async function deleteShare(target, opts = {}) {
    const backend = resolveShareBackend(opts);
    const githubUser = backend.kind === 'managed'
        ? backend.namespace
        : opts.githubUser || backend.namespace || undefined;
    const resolved = await resolveDeleteTarget(target, { githubUser });
    const endpoint = { baseUrl: backend.baseUrl, token: backend.token };
    const page = await deleteObject(endpoint, resolved.key, { deleter: opts.deleter, checker: opts.checker });
    if (!page.existedBefore) {
        if (opts.ifExists) {
            return { key: page.key, url: page.url, existedBefore: false, verified404: page.verified404, skipped: true };
        }
        throw new Error(`Nothing to delete — ${page.url} was already not found. Pass --if-exists to treat this as a no-op instead of an error.`);
    }
    if (!page.verified404) {
        throw new Error(`Delete reported success but ${page.url} still resolves — takedown NOT verified. Retry, or investigate the Worker/R2 directly.`);
    }
    const result = {
        key: page.key,
        url: page.url,
        existedBefore: page.existedBefore,
        verified404: page.verified404,
    };
    if (!opts.keepCover) {
        const cover = await deleteObject(endpoint, resolved.coverKey, { deleter: opts.deleter, checker: opts.checker });
        result.cover = {
            key: cover.key,
            url: cover.url,
            existedBefore: cover.existedBefore,
            verified404: cover.verified404,
        };
        // A missing cover is normal (non-HTML publishes, or --no-cover at publish
        // time never made one) — only a cover that existed and is still up is a bug.
        if (cover.existedBefore && !cover.verified404) {
            throw new Error(`Cover delete reported success but ${cover.url} still resolves — takedown NOT verified. Retry, or pass --keep-cover and delete it manually.`);
        }
    }
    if (!opts.keepRevisions) {
        const revisionKeys = await listRevisionKeys(endpoint, resolved.key, opts.fetchRevisions ?? defaultRevisionsFetch);
        if (revisionKeys.length > 0) {
            const revisions = [];
            for (const revisionKey of revisionKeys) {
                const rev = await deleteObject(endpoint, revisionKey, { deleter: opts.deleter, checker: opts.checker });
                revisions.push({ key: rev.key, url: rev.url, existedBefore: rev.existedBefore, verified404: rev.verified404 });
            }
            result.revisions = revisions;
            const stillUp = revisions.filter((r) => r.existedBefore && !r.verified404);
            if (stillUp.length > 0) {
                throw new Error(`Delete reported success but ${stillUp.length} retained revision(s) still resolve — takedown NOT verified. Retry, or pass --keep-revisions and delete them manually.`);
            }
        }
    }
    return result;
}
