// tools/git/github-issues-ops.ts — GitHub Issues ↔ Planu spec sync (SPEC-227)
// Uses native Node.js fetch (Node 22+). Requires GITHUB_TOKEN env var.
import { readFile } from 'node:fs/promises';
import { specStore } from '../../storage/index.js';
import { git, resolveProjectPath } from './git-helpers.js';
import { handleCreateSpec } from '../create-spec.js';
import { compactResult, compactError } from '../output-formatter.js';
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
const GITHUB_API = 'https://api.github.com';
// ---------------------------------------------------------------------------
// Auth helper
// ---------------------------------------------------------------------------
/** Returns GITHUB_TOKEN or throws a descriptive error (token never logged). */
function requireToken() {
    // eslint-disable-next-line @typescript-eslint/dot-notation -- env key with special chars
    const token = process.env['GITHUB_TOKEN'];
    if (!token) {
        throw new Error('GITHUB_TOKEN is not set. ' +
            'Run: export GITHUB_TOKEN=<your-personal-access-token> ' +
            '(requires repo scope for private repos, public_repo for public repos)');
    }
    return token;
}
// ---------------------------------------------------------------------------
// Repo detection helpers
// ---------------------------------------------------------------------------
/** Parse owner/repo from a git remote URL (SSH or HTTPS). */
function parseOwnerRepo(remoteUrl) {
    // HTTPS: https://github.com/owner/repo.git
    const httpsMatch = /github\.com[/:]([^/]+)\/([^/.]+)(?:\.git)?$/.exec(remoteUrl);
    if (httpsMatch?.[1] && httpsMatch[2]) {
        return { owner: httpsMatch[1], repo: httpsMatch[2] };
    }
    // SSH: git@github.com:owner/repo.git
    const sshMatch = /github\.com:([^/]+)\/([^/.]+)(?:\.git)?$/.exec(remoteUrl);
    if (sshMatch?.[1] && sshMatch[2]) {
        return { owner: sshMatch[1], repo: sshMatch[2] };
    }
    throw new Error(`Cannot parse owner/repo from remote URL: "${remoteUrl}". ` +
        'Provide repo as "owner/repo" via the repo parameter.');
}
/** Detect owner/repo from git remote origin. */
async function detectRepo(projectPath) {
    const { stdout } = await git(projectPath, ['remote', 'get-url', 'origin']);
    return parseOwnerRepo(stdout.trim());
}
/** Resolve owner/repo — from explicit param or git remote. */
async function resolveRepo(projectPath, repo) {
    if (repo) {
        const parts = repo.split('/');
        if (parts.length !== 2 || !parts[0] || !parts[1]) {
            throw new Error(`Invalid repo format "${repo}". Expected "owner/repo".`);
        }
        return { owner: parts[0], repo: parts[1] };
    }
    return detectRepo(projectPath);
}
// ---------------------------------------------------------------------------
// GitHub REST API wrappers
// ---------------------------------------------------------------------------
/** GET a single GitHub issue. */
async function fetchIssue(token, owner, repo, issueNumber) {
    const url = `${GITHUB_API}/repos/${owner}/${repo}/issues/${issueNumber}`;
    const res = await fetch(url, {
        headers: {
            Authorization: `Bearer ${token}`,
            Accept: 'application/vnd.github.v3+json',
            'User-Agent': 'planu-mcp-server',
        },
        signal: AbortSignal.timeout(10_000),
    });
    if (!res.ok) {
        const text = await res.text();
        throw new Error(`GitHub GET issue #${issueNumber} failed (${res.status}): ${text}`);
    }
    return res.json();
}
/** POST a new GitHub issue. */
async function createGitHubIssue(token, owner, repo, payload) {
    const url = `${GITHUB_API}/repos/${owner}/${repo}/issues`;
    const res = await fetch(url, {
        method: 'POST',
        headers: {
            Authorization: `Bearer ${token}`,
            Accept: 'application/vnd.github.v3+json',
            'Content-Type': 'application/json',
            'User-Agent': 'planu-mcp-server',
        },
        body: JSON.stringify(payload),
        signal: AbortSignal.timeout(10_000),
    });
    if (!res.ok) {
        const text = await res.text();
        throw new Error(`GitHub POST issue failed (${res.status}): ${text}`);
    }
    return res.json();
}
/** PATCH a GitHub issue (close, reopen, etc.). */
async function patchGitHubIssue(token, owner, repo, issueNumber, payload) {
    const url = `${GITHUB_API}/repos/${owner}/${repo}/issues/${issueNumber}`;
    const res = await fetch(url, {
        method: 'PATCH',
        headers: {
            Authorization: `Bearer ${token}`,
            Accept: 'application/vnd.github.v3+json',
            'Content-Type': 'application/json',
            'User-Agent': 'planu-mcp-server',
        },
        body: JSON.stringify(payload),
        signal: AbortSignal.timeout(10_000),
    });
    if (!res.ok) {
        const text = await res.text();
        throw new Error(`GitHub PATCH issue #${issueNumber} failed (${res.status}): ${text}`);
    }
}
/** POST a comment on a GitHub issue. */
async function postIssueComment(token, owner, repo, issueNumber, comment) {
    const url = `${GITHUB_API}/repos/${owner}/${repo}/issues/${issueNumber}/comments`;
    const res = await fetch(url, {
        method: 'POST',
        headers: {
            Authorization: `Bearer ${token}`,
            Accept: 'application/vnd.github.v3+json',
            'Content-Type': 'application/json',
            'User-Agent': 'planu-mcp-server',
        },
        body: JSON.stringify(comment),
        signal: AbortSignal.timeout(10_000),
    });
    if (!res.ok) {
        const text = await res.text();
        throw new Error(`GitHub POST comment on issue #${issueNumber} failed (${res.status}): ${text}`);
    }
}
// ---------------------------------------------------------------------------
// Spec markdown helpers
// ---------------------------------------------------------------------------
/** Extract acceptance criteria lines from a spec.md file (## Acceptance Criteria section). */
async function readAcceptanceCriteria(specPath) {
    try {
        const content = await readFile(specPath, 'utf-8');
        const lines = content.split('\n');
        const acStart = lines.findIndex((l) => /^#{1,3}\s+(acceptance criteria|criterios de aceptaci)/i.test(l));
        if (acStart === -1) {
            return [];
        }
        const criteria = [];
        for (let i = acStart + 1; i < lines.length; i++) {
            const line = lines[i];
            if (!line) {
                continue;
            }
            // Stop at next heading
            if (/^#{1,3}\s/.test(line)) {
                break;
            }
            // Collect list items or numbered items
            const match = /^[\s-]*(?:\d+\.)?\s*(?:AC[-\s]\d+:?\s*)?(.+)/.exec(line.trim());
            if (match?.[1]) {
                criteria.push(match[1].trim());
            }
        }
        return criteria;
    }
    catch {
        return [];
    }
}
/** Build a GitHub issue body from spec data. */
function buildIssueBody(specId, description, criteria, specStatus) {
    const parts = [
        `> Auto-synced from Planu spec **${specId}** (status: ${specStatus})`,
        '',
        description,
    ];
    if (criteria.length > 0) {
        parts.push('', '## Acceptance Criteria');
        for (const ac of criteria) {
            parts.push(`- [ ] ${ac}`);
        }
    }
    parts.push('', `---`, `_Managed by [Planu](https://planu.dev)_`);
    return parts.join('\n');
}
// ---------------------------------------------------------------------------
// Public handlers
// ---------------------------------------------------------------------------
/**
 * import-issue: Read a GitHub issue and create a draft Planu spec from it.
 */
export async function importFromGitHubIssue(args) {
    let token;
    try {
        token = requireToken();
    }
    catch (err) {
        return compactError(err instanceof Error ? err.message : String(err));
    }
    const projectPath = await resolveProjectPath(args.projectId);
    const { owner, repo } = await resolveRepo(projectPath, args.repo);
    const issue = await fetchIssue(token, owner, repo, args.issueNumber);
    const labels = issue.labels.map((l) => l.name);
    const description = issue.body ?? '';
    // Create spec via handleCreateSpec handler
    const result = await handleCreateSpec({
        projectPath,
        title: issue.title,
        description: description || `Imported from GitHub issue #${args.issueNumber}`,
        type: 'feature',
        scope: 'feature',
        tags: labels,
    });
    // Extract specId and specPath from structuredContent
    const structured = result.structuredContent;
    const specId = structured?.specId ?? '';
    const specPath = structured?.specPath ?? '';
    const importResult = {
        action: 'import-issue',
        specId,
        issueNumber: issue.number,
        issueUrl: issue.html_url,
        specPath,
        message: `Imported GitHub issue #${issue.number} "${issue.title}" as draft spec ${specId || '(created)'}`,
    };
    const text = [
        importResult.message,
        '',
        `**action**: ${importResult.action}`,
        `**specId**: ${importResult.specId}`,
        `**issueNumber**: ${String(importResult.issueNumber)}`,
        `**issueUrl**: ${importResult.issueUrl}`,
        `**specPath**: ${importResult.specPath}`,
    ].join('\n');
    return compactResult(text);
}
/**
 * export-to-issue: Read a Planu spec and create a GitHub issue with ACs as checklist.
 */
export async function exportSpecToGitHubIssue(args) {
    let token;
    try {
        token = requireToken();
    }
    catch (err) {
        return compactError(err instanceof Error ? err.message : String(err));
    }
    const projectPath = await resolveProjectPath(args.projectId);
    const { owner, repo } = await resolveRepo(projectPath, args.repo);
    const spec = await specStore.getSpec(args.projectId, args.specId);
    if (!spec) {
        return compactError(`Spec '${args.specId}' not found`);
    }
    const criteria = await readAcceptanceCriteria(spec.specPath);
    const description = `**Type:** ${spec.type} | **Scope:** ${spec.scope} | **Risk:** ${spec.risk}`;
    const body = buildIssueBody(args.specId, description, criteria, spec.status);
    const issue = await createGitHubIssue(token, owner, repo, {
        title: `[${args.specId}] ${spec.title}`,
        body,
        labels: spec.tags,
    });
    const exportResult = {
        action: 'export-to-issue',
        specId: args.specId,
        issueNumber: issue.number,
        issueUrl: issue.html_url,
        message: `Exported spec ${args.specId} to GitHub issue #${issue.number}: ${issue.html_url}`,
    };
    const exportText = [
        exportResult.message,
        '',
        `**action**: ${exportResult.action}`,
        `**specId**: ${exportResult.specId}`,
        `**issueNumber**: ${String(exportResult.issueNumber)}`,
        `**issueUrl**: ${exportResult.issueUrl}`,
    ].join('\n');
    return compactResult(exportText);
}
/**
 * sync-issue: Sync spec status with a GitHub issue.
 * - spec.status=done → close the issue
 * - spec.status=approved → post a comment on the issue
 */
export async function syncSpecWithGitHubIssue(args) {
    let token;
    try {
        token = requireToken();
    }
    catch (err) {
        return compactError(err instanceof Error ? err.message : String(err));
    }
    const projectPath = await resolveProjectPath(args.projectId);
    const { owner, repo } = await resolveRepo(projectPath, args.repo);
    const spec = await specStore.getSpec(args.projectId, args.specId);
    if (!spec) {
        return compactError(`Spec '${args.specId}' not found`);
    }
    const issue = await fetchIssue(token, owner, repo, args.issueNumber);
    let syncedAction = 'none';
    if (spec.status === 'done') {
        if (issue.state !== 'closed') {
            await patchGitHubIssue(token, owner, repo, args.issueNumber, { state: 'closed' });
        }
        syncedAction = 'closed';
    }
    else if (spec.status === 'approved') {
        await postIssueComment(token, owner, repo, args.issueNumber, {
            body: `**Planu spec ${args.specId} is now approved** and implementation has started.\n\n` +
                `_Status: approved | Risk: ${spec.risk}_`,
        });
        syncedAction = 'commented';
    }
    const syncResult = {
        action: 'sync-issue',
        specId: args.specId,
        issueNumber: issue.number,
        issueUrl: issue.html_url,
        syncedAction,
        message: `Synced spec ${args.specId} (${spec.status}) with issue #${args.issueNumber}: action=${syncedAction}`,
    };
    const syncText = [
        syncResult.message,
        '',
        `**action**: ${syncResult.action}`,
        `**specId**: ${syncResult.specId}`,
        `**issueNumber**: ${String(syncResult.issueNumber)}`,
        `**syncedAction**: ${syncResult.syncedAction}`,
        `**issueUrl**: ${syncResult.issueUrl}`,
    ].join('\n');
    return compactResult(syncText);
}
//# sourceMappingURL=github-issues-ops.js.map