import { getSession } from "./neo4j.js";
import { decrypt } from "./crypto.js";
// ─────────────────────────────────────────────────────────────────
// Loop API V2 HTTP Client + Cross-Team Aggregation Engine
//
//   ┌─ Load LoopTeamKey nodes (Neo4j) ─┐
//   │                                    │
//   ▼                                    │
//   For each key:                        │
//     1. Decrypt API key                 │
//     2. Check permissions               │
//     3. HTTP GET/POST/PUT → Loop API V2 │
//   │                                    │
//   ▼                                    │
//   Promise.allSettled → merge results   │
//   Log aggregate summary                │
// ─────────────────────────────────────────────────────────────────
const LOOP_BASE_URL = "https://api.loop.software";
const REQUEST_TIMEOUT_MS = 10_000;
const DEFAULT_LIMIT_PER_TEAM = 50;
const DEFAULT_LIMIT_TOTAL = 200;
function isEnvelope(val) {
    return (val !== null &&
        typeof val === "object" &&
        !Array.isArray(val) &&
        "statusCode" in val &&
        "httpResponseCode" in val &&
        "data" in val);
}
function unwrapEnvelope(parsed, toolName, teamName) {
    if (!isEnvelope(parsed)) {
        return parsed;
    }
    if (parsed.statusCode !== "success") {
        const errMsg = parsed.errors?.length
            ? parsed.errors.join("; ")
            : `statusCode=${parsed.statusCode}`;
        throw new Error(`Loop API error for team=${teamName} tool=${toolName}: ${errMsg} (httpResponseCode=${parsed.httpResponseCode})`);
    }
    if (parsed.warnings?.length) {
        console.error(`[loop] ${toolName} team=${teamName} warnings: ${parsed.warnings.join("; ")}`);
    }
    return parsed.data;
}
/**
 * Load all LoopTeamKey nodes for an account from Neo4j.
 */
export async function loadTeamKeys(accountId) {
    const session = getSession();
    try {
        const result = await session.run(`MATCH (k:LoopTeamKey {accountId: $accountId})
       RETURN k.teamName AS teamName, k.encryptedKey AS encryptedKey,
              k.teamAddress AS teamAddress, k.agentId AS agentId,
              k.permissions AS permissions, k.createdAt AS createdAt,
              k.updatedAt AS updatedAt
       ORDER BY k.teamName`, { accountId });
        return result.records.map((r) => ({
            teamName: r.get("teamName"),
            encryptedKey: r.get("encryptedKey"),
            teamAddress: r.get("teamAddress") ?? "",
            agentId: r.get("agentId") ?? "",
            permissions: r.get("permissions") ?? [],
            createdAt: r.get("createdAt") ?? "",
            updatedAt: r.get("updatedAt") ?? "",
        }));
    }
    finally {
        await session.close();
    }
}
/**
 * Make a GET request to the Loop API V2.
 */
export async function loopGet(apiKey, path, toolName, teamName) {
    const url = `${LOOP_BASE_URL}${path}`;
    const start = Date.now();
    const response = await fetch(url, {
        headers: { "X-Api-Key": apiKey, "Accept": "application/json" },
        signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
    });
    const duration = Date.now() - start;
    console.error(`[loop] ${toolName} team=${teamName} endpoint=GET ${path} status=${response.status} duration=${duration}ms`);
    if (response.status === 204) {
        return [];
    }
    if (!response.ok) {
        throw new LoopApiError(response.status, teamName, path);
    }
    const text = await response.text();
    try {
        const parsed = JSON.parse(text);
        return unwrapEnvelope(parsed, toolName, teamName);
    }
    catch (err) {
        if (err instanceof SyntaxError) {
            throw new Error(`Loop returned non-JSON response for team=${teamName} path=${path} ` +
                `(status=${response.status}, length=${text.length})`);
        }
        throw err;
    }
}
/**
 * Make a POST request to the Loop API V2.
 * For write operations: create viewings, record feedback, submit enquiries, etc.
 * Returns parsed JSON response. Treats 204 as success with empty result.
 */
export async function loopPost(apiKey, path, body, toolName, teamName) {
    const url = `${LOOP_BASE_URL}${path}`;
    const start = Date.now();
    const response = await fetch(url, {
        method: "POST",
        headers: {
            "X-Api-Key": apiKey,
            "Accept": "application/json",
            "Content-Type": "application/json",
        },
        body: JSON.stringify(body),
        signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
    });
    const duration = Date.now() - start;
    console.error(`[loop] WRITE ${toolName} team=${teamName} endpoint=POST ${path} status=${response.status} duration=${duration}ms`);
    if (response.status === 204) {
        return { success: true };
    }
    if (!response.ok) {
        const errorBody = await response.text().catch(() => "");
        throw new LoopApiError(response.status, teamName, path, errorBody);
    }
    const text = await response.text();
    try {
        const parsed = JSON.parse(text);
        return unwrapEnvelope(parsed, toolName, teamName);
    }
    catch (err) {
        if (err instanceof SyntaxError) {
            throw new Error(`Loop returned non-JSON response for team=${teamName} path=${path} ` +
                `(status=${response.status}, length=${text.length})`);
        }
        throw err;
    }
}
/**
 * Make a PUT request to the Loop API V2.
 * For update operations: batch matching, submit quotes, update preferences, etc.
 */
export async function loopPut(apiKey, path, body, toolName, teamName) {
    const url = `${LOOP_BASE_URL}${path}`;
    const start = Date.now();
    const response = await fetch(url, {
        method: "PUT",
        headers: {
            "X-Api-Key": apiKey,
            "Accept": "application/json",
            "Content-Type": "application/json",
        },
        body: JSON.stringify(body),
        signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
    });
    const duration = Date.now() - start;
    console.error(`[loop] WRITE ${toolName} team=${teamName} endpoint=PUT ${path} status=${response.status} duration=${duration}ms`);
    if (response.status === 204) {
        return { success: true };
    }
    if (!response.ok) {
        const errorBody = await response.text().catch(() => "");
        throw new LoopApiError(response.status, teamName, path, errorBody);
    }
    const text = await response.text();
    try {
        const parsed = JSON.parse(text);
        return unwrapEnvelope(parsed, toolName, teamName);
    }
    catch (err) {
        if (err instanceof SyntaxError) {
            throw new Error(`Loop returned non-JSON response for team=${teamName} path=${path} ` +
                `(status=${response.status}, length=${text.length})`);
        }
        throw err;
    }
}
export class LoopApiError extends Error {
    status;
    teamName;
    path;
    responseBody;
    constructor(status, teamName, path, responseBody) {
        const detail = responseBody ? ` — ${responseBody.slice(0, 200)}` : "";
        super(`Loop API returned ${status} for team=${teamName} path=${path}${detail}`);
        this.status = status;
        this.teamName = teamName;
        this.path = path;
        this.responseBody = responseBody;
        this.name = "LoopApiError";
    }
}
/**
 * Fan out a request across all registered teams for an account, merging results.
 *
 * Each tool provides a `buildRequest` function that takes a decrypted API key
 * and returns the data for that team. The aggregation engine handles:
 * - Key loading and decryption
 * - Permission checking
 * - Concurrent fan-out via Promise.allSettled
 * - Result merging with teamName tags
 * - Aggregate logging
 *
 * When `teamName` is provided, only that team is queried (no fan-out).
 */
export async function aggregateAcrossTeams(accountId, endpointGroup, toolName, buildRequest, options) {
    const allKeys = await loadTeamKeys(accountId);
    if (allKeys.length === 0) {
        return { results: [], failures: [], skipped: [], totalTeams: 0 };
    }
    // Filter to specific team if requested
    const keys = options?.teamName
        ? allKeys.filter((k) => k.teamName === options.teamName)
        : allKeys;
    if (keys.length === 0 && options?.teamName) {
        return {
            results: [],
            failures: [{ teamName: options.teamName, reason: "Team not found" }],
            skipped: [],
            totalTeams: allKeys.length,
        };
    }
    const limitPerTeam = options?.limitPerTeam ?? DEFAULT_LIMIT_PER_TEAM;
    const limitTotal = options?.limitTotal ?? DEFAULT_LIMIT_TOTAL;
    const results = [];
    const failures = [];
    const skipped = [];
    // Check permissions and build request promises
    const tasks = [];
    for (const key of keys) {
        if (!key.permissions.includes(endpointGroup)) {
            console.error(`[loop] ${toolName} team=${key.teamName} — permission denied (requires: ${endpointGroup})`);
            skipped.push({
                teamName: key.teamName,
                reason: `No ${endpointGroup} permission`,
            });
            continue;
        }
        let apiKey;
        try {
            apiKey = decrypt(key.encryptedKey);
        }
        catch (err) {
            const msg = err instanceof Error ? err.message : String(err);
            console.error(`[loop] decrypt failed for team=${key.teamName}: ${msg}`);
            failures.push({ teamName: key.teamName, reason: `Decryption failed` });
            continue;
        }
        tasks.push({ key, promise: buildRequest(apiKey, key.teamName) });
    }
    // Fan out concurrent requests
    const settled = await Promise.allSettled(tasks.map((t) => t.promise));
    for (let i = 0; i < settled.length; i++) {
        const outcome = settled[i];
        const teamName = tasks[i].key.teamName;
        if (outcome.status === "fulfilled") {
            const data = Array.isArray(outcome.value)
                ? outcome.value.slice(0, limitPerTeam)
                : [];
            results.push({ teamName, data });
        }
        else {
            const reason = outcome.reason instanceof LoopApiError
                ? `HTTP ${outcome.reason.status}`
                : outcome.reason instanceof Error
                    ? outcome.reason.message
                    : String(outcome.reason);
            console.error(`[loop] ${toolName} team=${teamName} failed: ${reason}`);
            failures.push({ teamName, reason });
        }
    }
    // Apply total limit across all teams
    let totalItems = 0;
    for (const r of results) {
        const remaining = limitTotal - totalItems;
        if (remaining <= 0) {
            r.data = [];
        }
        else if (r.data.length > remaining) {
            r.data = r.data.slice(0, remaining);
        }
        totalItems += r.data.length;
    }
    console.error(`[loop] aggregate tool=${toolName} teams=${keys.length} ` +
        `succeeded=${results.length} failed=${failures.length} skipped=${skipped.length}`);
    return { results, failures, skipped, totalTeams: allKeys.length };
}
/**
 * Resolve a single team's API key with permission checking.
 * Handles key loading, decryption, and permission validation.
 * Used for both reads and writes that target a specific team — no fan-out.
 */
export async function withTeamKey(accountId, teamName, endpointGroup, toolName, execute) {
    const allKeys = await loadTeamKeys(accountId);
    if (allKeys.length === 0) {
        throw new Error("No Loop teams registered. Use loop-key-register to add team API keys.");
    }
    const key = allKeys.find((k) => k.teamName === teamName);
    if (!key) {
        const available = allKeys.map((k) => k.teamName).join(", ");
        throw new Error(`Team "${teamName}" not found. Available teams: ${available}`);
    }
    if (!key.permissions.includes(endpointGroup)) {
        throw new Error(`Team "${teamName}" does not have ${endpointGroup} permission. ` +
            `Current permissions: ${key.permissions.join(", ")}`);
    }
    const apiKey = decrypt(key.encryptedKey);
    return execute(apiKey);
}
/**
 * Format an aggregation result into a human-readable text response.
 */
export function formatAggregationResult(result, formatItem, entityName) {
    if (result.totalTeams === 0) {
        return "No Loop teams registered. Use loop-key-register to add team API keys.";
    }
    const allSkippedOrFailed = result.results.length === 0 && result.failures.length + result.skipped.length > 0;
    if (allSkippedOrFailed) {
        const reasons = [
            ...result.failures.map((f) => `${f.teamName}: ${f.reason}`),
            ...result.skipped.map((s) => `${s.teamName}: ${s.reason}`),
        ];
        return `All teams failed or were skipped:\n${reasons.join("\n")}`;
    }
    const lines = [];
    let totalItems = 0;
    for (const teamResult of result.results) {
        if (teamResult.data.length === 0)
            continue;
        totalItems += teamResult.data.length;
        lines.push(`\n## ${teamResult.teamName}`);
        for (const item of teamResult.data) {
            lines.push(formatItem(item, teamResult.teamName));
        }
    }
    if (totalItems === 0) {
        const teamCount = result.results.length === 1
            ? result.results[0].teamName
            : `${result.results.length} teams`;
        return `No ${entityName} found across ${teamCount}.`;
    }
    let header = `${totalItems} ${entityName} across ${result.results.length} team(s):`;
    const parts = [header, ...lines];
    if (result.failures.length > 0) {
        parts.push(`\n---\nNote: ${result.failures.length} team(s) failed — check server.log for details.`);
    }
    if (result.skipped.length > 0) {
        parts.push(`Note: ${result.skipped.length} team(s) skipped (insufficient permissions).`);
    }
    return parts.join("\n");
}
//# sourceMappingURL=loop-api.js.map