import fs from "node:fs";
import fsp from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { z } from "zod";
import { tool } from "@anthropic-ai/claude-agent-sdk";
import { generateSync, generateSecret, verifySync, generateURI, createGuardrails, } from "otplib";
// RFC 6238 recommends 128-bit (16-byte) minimum secrets, but many real services
// (GitHub, Discord, etc.) use 80-bit (10-byte) secrets. Relax the minimum to 1 byte.
const lenientGuardrails = createGuardrails({ MIN_SECRET_BYTES: 1 });
// jsqr has no type declarations
import _jsQR from "jsqr";
const jsQR = _jsQR;
import { loadTOTPSecrets, saveTOTPSecrets, } from "../config/index.js";
import { getImageDimensionsFromFile } from "./screenshot.js";
import { execWithTimeout, HEAVY_EXEC_TIMEOUT_MS } from "../utils/exec.js";
function ok(text) {
    return { content: [{ type: "text", text }] };
}
function err(text) {
    return { content: [{ type: "text", text }], isError: true };
}
const VALID_ALGORITHMS = ["sha1", "sha256", "sha512"];
function normalizeAlgorithm(alg) {
    const lower = (alg?.toLowerCase() ?? "sha1");
    if (VALID_ALGORITHMS.includes(lower))
        return lower;
    return "sha1";
}
/**
 * Parse an otpauth:// URI into its components.
 * Format: otpauth://totp/LABEL?secret=BASE32&issuer=ISSUER&digits=6&period=30&algorithm=SHA1
 */
function parseOtpauthURI(uri) {
    try {
        const url = new URL(uri);
        if (url.protocol !== "otpauth:")
            return null;
        const secret = url.searchParams.get("secret");
        if (!secret)
            return null;
        // Label is in the path: /ISSUER:ACCOUNT or /ACCOUNT
        const label = decodeURIComponent(url.pathname.replace(/^\/\/totp\//, "").replace(/^\//, ""));
        let issuer = url.searchParams.get("issuer") ?? undefined;
        let account;
        if (label.includes(":")) {
            const [labelIssuer, labelAccount] = label.split(":", 2);
            issuer = issuer ?? labelIssuer;
            account = labelAccount;
        }
        else if (label) {
            account = label;
        }
        const digits = url.searchParams.get("digits");
        const period = url.searchParams.get("period");
        const algorithm = url.searchParams.get("algorithm");
        return {
            secret,
            issuer,
            account,
            digits: digits ? Number(digits) : undefined,
            period: period ? Number(period) : undefined,
            algorithm: algorithm ?? undefined,
        };
    }
    catch {
        return null;
    }
}
function handleStore(args) {
    let entry;
    if (args.uri) {
        const parsed = parseOtpauthURI(args.uri);
        if (!parsed) {
            return err("Failed to parse otpauth:// URI. Expected format: otpauth://totp/LABEL?secret=BASE32SECRET");
        }
        const service = args.service ?? parsed.issuer ?? parsed.account ?? "unknown";
        entry = {
            service,
            secret: parsed.secret,
            issuer: args.issuer ?? parsed.issuer,
            account: args.account ?? parsed.account,
            digits: parsed.digits,
            period: parsed.period,
            algorithm: parsed.algorithm,
        };
    }
    else if (args.secret && args.service) {
        entry = {
            service: args.service,
            secret: args.secret,
            issuer: args.issuer,
            account: args.account,
        };
    }
    else {
        return err("Either 'uri' (otpauth:// URI) or both 'service' and 'secret' are required.");
    }
    const secrets = loadTOTPSecrets();
    const existingIdx = secrets.findIndex((s) => s.service.toLowerCase() === entry.service.toLowerCase());
    const fullEntry = {
        ...entry,
        storedAt: new Date().toISOString(),
    };
    const isUpdate = existingIdx >= 0;
    if (isUpdate) {
        secrets[existingIdx] = fullEntry;
    }
    else {
        secrets.push(fullEntry);
    }
    saveTOTPSecrets(secrets);
    // Generate a code immediately to verify the secret works
    const action = isUpdate ? "Updated" : "Stored new";
    try {
        const token = generateSync({
            secret: entry.secret,
            digits: entry.digits ?? 6,
            period: entry.period ?? 30,
            algorithm: normalizeAlgorithm(entry.algorithm),
            guardrails: lenientGuardrails,
        });
        return ok(`${action} TOTP secret for "${entry.service}". Current code: ${token} (valid for ~${entry.period ?? 30}s)`);
    }
    catch (e) {
        return ok(`${action} TOTP secret for "${entry.service}", but code generation failed: ${e instanceof Error ? e.message : String(e)}. The secret may be invalid.`);
    }
}
function handleGenerate(args) {
    if (!args.service) {
        return err("'service' is required for generate command.");
    }
    const service = args.service;
    const secrets = loadTOTPSecrets();
    const entry = secrets.find((s) => s.service.toLowerCase() === service.toLowerCase());
    if (!entry) {
        const available = secrets.map((s) => s.service).join(", ");
        return err(`No TOTP secret found for "${args.service}".${available ? ` Available: ${available}` : " No secrets stored."}`);
    }
    try {
        const token = generateSync({
            secret: entry.secret,
            digits: entry.digits ?? 6,
            period: entry.period ?? 30,
            algorithm: normalizeAlgorithm(entry.algorithm),
            guardrails: lenientGuardrails,
        });
        const period = entry.period ?? 30;
        const epoch = Math.floor(Date.now() / 1000);
        const remaining = period - (epoch % period);
        return ok(`${token} (expires in ${remaining}s)`);
    }
    catch (e) {
        return err(`Failed to generate TOTP code: ${e instanceof Error ? e.message : String(e)}`);
    }
}
function handleVerify(args) {
    if (!args.service || !args.token) {
        return err("'service' and 'token' are required for verify command.");
    }
    const service = args.service;
    const secrets = loadTOTPSecrets();
    const entry = secrets.find((s) => s.service.toLowerCase() === service.toLowerCase());
    if (!entry) {
        return err(`No TOTP secret found for "${args.service}".`);
    }
    try {
        const result = verifySync({
            token: args.token,
            secret: entry.secret,
            digits: entry.digits ?? 6,
            period: entry.period ?? 30,
            algorithm: normalizeAlgorithm(entry.algorithm),
            guardrails: lenientGuardrails,
        });
        return ok(result.valid ? "Token is valid." : "Token is invalid.");
    }
    catch (e) {
        return err(`Verification failed: ${e instanceof Error ? e.message : String(e)}`);
    }
}
function handleList() {
    const secrets = loadTOTPSecrets();
    if (secrets.length === 0) {
        return ok("No TOTP secrets stored.");
    }
    const lines = secrets.map((s) => {
        const parts = [s.service];
        if (s.issuer)
            parts.push(`issuer: ${s.issuer}`);
        if (s.account)
            parts.push(`account: ${s.account}`);
        parts.push(`stored: ${s.storedAt}`);
        return `- ${parts.join(" | ")}`;
    });
    return ok(`Stored TOTP secrets (${secrets.length}):\n${lines.join("\n")}`);
}
function handleDelete(args) {
    if (!args.service) {
        return err("'service' is required for delete command.");
    }
    const service = args.service;
    const secrets = loadTOTPSecrets();
    const idx = secrets.findIndex((s) => s.service.toLowerCase() === service.toLowerCase());
    if (idx < 0) {
        return err(`No TOTP secret found for "${args.service}".`);
    }
    const removed = secrets.splice(idx, 1)[0];
    saveTOTPSecrets(secrets);
    return ok(`TOTP secret for "${removed.service}" deleted.`);
}
function handleGenerateSecret() {
    const secret = generateSecret();
    return ok(`Generated TOTP secret: ${secret}\n\nUse the 'store' command to save it for a service.`);
}
function handleGetURI(args) {
    if (!args.service) {
        return err("'service' is required for get_uri command.");
    }
    const service = args.service;
    const secrets = loadTOTPSecrets();
    const entry = secrets.find((s) => s.service.toLowerCase() === service.toLowerCase());
    if (!entry) {
        return err(`No TOTP secret found for "${args.service}".`);
    }
    const uri = generateURI({
        secret: entry.secret,
        issuer: entry.issuer ?? entry.service,
        label: entry.account ?? entry.service,
        digits: entry.digits ?? 6,
        period: entry.period ?? 30,
        algorithm: normalizeAlgorithm(entry.algorithm),
    });
    return ok(uri);
}
/**
 * Convert an image file to raw RGBA pixel data using ffmpeg.
 * Returns the raw buffer along with image dimensions.
 */
async function imageToRawRGBA(imagePath) {
    // Get dimensions using sips (macOS) / identify (Linux) via shared helper
    const dims = await getImageDimensionsFromFile(imagePath);
    if (!dims) {
        throw new Error("Could not read image dimensions");
    }
    // Use ffmpeg to convert to raw RGBA pixels
    const tmpRaw = path.join(os.tmpdir(), `visionclaw-qr-${Date.now()}.rgba`);
    try {
        await execWithTimeout(`ffmpeg -y -i "${imagePath}" -pix_fmt rgba -f rawvideo "${tmpRaw}"`, { timeout: HEAVY_EXEC_TIMEOUT_MS });
        const data = await fsp.readFile(tmpRaw);
        return { data, width: dims.width, height: dims.height };
    }
    finally {
        try {
            await fsp.unlink(tmpRaw);
        }
        catch { /* noop */ }
    }
}
async function handleDecodeQR(args) {
    if (!args.image_path) {
        return err("'image_path' is required for decode_qr command. Provide the path to a screenshot or image containing a QR code.");
    }
    if (!fs.existsSync(args.image_path)) {
        return err(`File not found: ${args.image_path}`);
    }
    try {
        const { data, width, height } = await imageToRawRGBA(args.image_path);
        const qr = jsQR(new Uint8ClampedArray(data.buffer), width, height);
        if (!qr) {
            return err("No QR code found in the image. Make sure the QR code is clearly visible and not too small.");
        }
        const decoded = qr.data;
        // If it's an otpauth:// URI, parse and optionally store it
        if (decoded.startsWith("otpauth://")) {
            const parsed = parseOtpauthURI(decoded);
            if (!parsed) {
                return ok(`Decoded QR code contains an otpauth URI but it could not be parsed:\n${decoded}`);
            }
            const infoParts = [`URI: ${decoded}`];
            if (parsed.issuer)
                infoParts.push(`Issuer: ${parsed.issuer}`);
            if (parsed.account)
                infoParts.push(`Account: ${parsed.account}`);
            // Auto-store if service name provided or can be inferred
            const service = args.service ?? parsed.issuer ?? parsed.account;
            if (service) {
                const storeResult = handleStore({ service, uri: decoded });
                return ok(`QR code decoded successfully!\n${infoParts.join("\n")}\n\n${storeResult.content[0] && "text" in storeResult.content[0] ? storeResult.content[0].text : ""}`);
            }
            return ok(`QR code decoded successfully!\n${infoParts.join("\n")}\n\nUse the 'store' command with this URI to save it.`);
        }
        return ok(`QR code decoded (not an otpauth URI):\n${decoded}`);
    }
    catch (e) {
        return err(`Failed to decode QR code: ${e instanceof Error ? e.message : String(e)}`);
    }
}
export const manageTOTPTool = tool("manage_totp", `Manage TOTP (Time-based One-Time Password) secrets for 2FA authentication.

Commands:
- store: Save a TOTP secret. Provide either a 'uri' (otpauth:// URI from QR code) or 'service' + 'secret'.
- generate: Generate the current 6-digit TOTP code for a service.
- verify: Verify a TOTP token against a stored secret.
- list: List all stored TOTP services (secrets are not shown).
- delete: Remove a stored TOTP secret.
- generate_secret: Generate a new random TOTP secret.
- get_uri: Get the otpauth:// URI for a stored service (useful for QR code generation).
- decode_qr: Decode a QR code from an image file. If the QR contains an otpauth:// URI, it is automatically stored.

When encountering a 2FA setup screen with a QR code, take a screenshot, then use 'decode_qr' with the image path to extract and store the TOTP secret. When a 2FA code is needed, use the 'generate' command.`, {
    command: z
        .enum(["store", "generate", "verify", "list", "delete", "generate_secret", "get_uri", "decode_qr"])
        .describe("TOTP command to execute"),
    service: z
        .string()
        .optional()
        .describe("Service name (e.g. 'github', 'google', 'aws')"),
    secret: z
        .string()
        .optional()
        .describe("Base32-encoded TOTP secret (for 'store' command)"),
    uri: z
        .string()
        .optional()
        .describe("otpauth:// URI (for 'store' command, typically from QR code)"),
    token: z
        .string()
        .optional()
        .describe("6-digit TOTP token (for 'verify' command)"),
    issuer: z
        .string()
        .optional()
        .describe("Service issuer name (for 'store' command)"),
    account: z
        .string()
        .optional()
        .describe("Account identifier, e.g. email (for 'store' command)"),
    image_path: z
        .string()
        .optional()
        .describe("Path to image file containing a QR code (for 'decode_qr' command)"),
}, async (args) => {
    try {
        switch (args.command) {
            case "store":
                return handleStore(args);
            case "generate":
                return handleGenerate(args);
            case "verify":
                return handleVerify(args);
            case "list":
                return handleList();
            case "delete":
                return handleDelete(args);
            case "generate_secret":
                return handleGenerateSecret();
            case "get_uri":
                return handleGetURI(args);
            case "decode_qr":
                return await handleDecodeQR(args);
            default:
                return err(`Unknown command: ${args.command}`);
        }
    }
    catch (e) {
        return err(`TOTP error: ${e instanceof Error ? e.message : String(e)}`);
    }
});
//# sourceMappingURL=totp.js.map