/**
 * LinkedInPublisher ; LinkedIn API v2 UGC Posts publisher.
 *
 * Auth: Bearer token (OAuth 2.0 user token) via LINKEDIN_ACCESS_TOKEN env var.
 * Author URN: LINKEDIN_AUTHOR_URN env var (e.g. "urn:li:person:abc123").
 * Endpoint: POST https://api.linkedin.com/v2/ugcPosts
 * Limit: 3000 chars per post (LinkedIn platform limit).
 *
 * HTTP contract:
 *   - 401 : throws auth error, no retry
 *   - 429 : exponential backoff, max 3 retries
 *   - 5xx : throws with HTTP body
 *   - 2xx : returns posted_url + posted_id from the X-RestLi-Id response header
 *
 * @public
 */
const LINKEDIN_POST_MAX_CHARS = 3000;
const LINKEDIN_API_BASE = "https://api.linkedin.com/v2/ugcPosts";
const LINKEDIN_FEED_PREFIX = "https://www.linkedin.com/feed/update/";
function defaultSleep(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
}
export class LinkedInPublisher {
    channel = "linkedin";
    cfg;
    sleep;
    random;
    constructor(cfg = {}, options = {}) {
        this.cfg = cfg;
        this.sleep = options.sleep ?? defaultSleep;
        this.random = options.random ?? Math.random;
    }
    resolveToken() {
        return this.cfg.accessToken ?? process.env["LINKEDIN_ACCESS_TOKEN"];
    }
    resolveAuthorUrn() {
        return this.cfg.authorUrn ?? process.env["LINKEDIN_AUTHOR_URN"];
    }
    async isConfigured() {
        return Boolean(this.resolveToken()) && Boolean(this.resolveAuthorUrn());
    }
    async publish(input) {
        const token = this.resolveToken();
        if (!token) {
            return {
                status: "failed",
                channel: "linkedin",
                error: "LINKEDIN_ACCESS_TOKEN is required ; set it as env var or pass accessToken in config",
            };
        }
        const authorUrn = this.resolveAuthorUrn();
        if (!authorUrn) {
            return {
                status: "failed",
                channel: "linkedin",
                error: "LINKEDIN_AUTHOR_URN is required ; set it as env var or pass authorUrn in config",
            };
        }
        const p = input.payload;
        const rawText = typeof p["text"] === "string" ? p["text"] : "";
        if (!rawText) {
            return {
                status: "failed",
                channel: "linkedin",
                error: "linkedin platform requires payload.text",
            };
        }
        // Append mentions to text when provided (LinkedIn UGC API has no dedicated
        // mention field outside Shares v2 ; surface them inline).
        const mentions = Array.isArray(p["mentions"])
            ? p["mentions"].filter((m) => typeof m === "string")
            : [];
        const text = mentions.length > 0 ? `${rawText}\n${mentions.join(" ")}` : rawText;
        if (text.length > LINKEDIN_POST_MAX_CHARS) {
            return {
                status: "failed",
                channel: "linkedin",
                error: `linkedin post exceeds 3000-char limit (got ${text.length})`,
            };
        }
        const body = {
            author: authorUrn,
            lifecycleState: "PUBLISHED",
            specificContent: {
                "com.linkedin.ugc.ShareContent": {
                    shareCommentary: { text },
                    shareMediaCategory: "NONE",
                },
            },
            visibility: {
                "com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC",
            },
        };
        const MAX_RETRIES = 3;
        let lastError = "";
        for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
            let res;
            try {
                res = await fetch(LINKEDIN_API_BASE, {
                    method: "POST",
                    headers: {
                        "Content-Type": "application/json",
                        Authorization: `Bearer ${token}`,
                        "X-Restli-Protocol-Version": "2.0.0",
                    },
                    body: JSON.stringify(body),
                    signal: AbortSignal.timeout(20_000),
                });
            }
            catch (err) {
                return {
                    status: "failed",
                    channel: "linkedin",
                    error: `linkedin fetch error: ${err.message}`,
                };
            }
            if (res.status === 401) {
                const bodyText = (await res.text().catch(() => "")).slice(0, 256);
                return {
                    status: "failed",
                    channel: "linkedin",
                    error: `linkedin auth error (401): ${bodyText}`,
                };
            }
            if (res.status === 429) {
                if (attempt === MAX_RETRIES) {
                    return {
                        status: "failed",
                        channel: "linkedin",
                        error: "linkedin rate-limited (429): max retries exceeded",
                    };
                }
                // Exponential backoff with jitter: base 2s, +0-1s jitter, doubled each retry.
                const backoffMs = (2_000 + this.random() * 1_000) * Math.pow(2, attempt);
                await this.sleep(backoffMs);
                lastError = "429 rate limited";
                continue;
            }
            if (res.status >= 500) {
                const bodyText = (await res.text().catch(() => "")).slice(0, 512);
                return {
                    status: "failed",
                    channel: "linkedin",
                    error: `linkedin server error (${res.status}): ${bodyText}`,
                };
            }
            if (res.ok) {
                // LinkedIn returns the post URN in the X-RestLi-Id header on 201.
                const urn = res.headers.get("x-restli-id") ?? res.headers.get("X-RestLi-Id");
                const posted_id = urn ?? undefined;
                const posted_url = urn ? `${LINKEDIN_FEED_PREFIX}${encodeURIComponent(urn)}` : undefined;
                return {
                    status: "published",
                    channel: "linkedin",
                    posted_at: new Date().toISOString(),
                    ...(posted_url ? { posted_url } : {}),
                    ...(posted_id ? { posted_id } : {}),
                };
            }
            // Other 4xx errors (400, 403, etc.) ; no retry.
            const bodyText = (await res.text().catch(() => "")).slice(0, 256);
            return {
                status: "failed",
                channel: "linkedin",
                error: `linkedin error (${res.status}): ${bodyText}`,
            };
        }
        // Should not be reached but TypeScript requires a return.
        return {
            status: "failed",
            channel: "linkedin",
            error: `linkedin: ${lastError}`,
        };
    }
}
//# sourceMappingURL=linkedin.js.map