import { ServiceBubble } from '../../../types/service-bubble-class.js';
import { CredentialType } from '@bubblelab/shared-schemas';
import { LinearParamsSchema, LinearResultSchema, } from './linear.schema.js';
import { makeGraphQLRequest, buildSearchQuery, buildSearchByTextQuery, buildGetIssueQuery, buildCreateIssueMutation, buildUpdateIssueMutation, buildListTeamsQuery, buildListProjectsQuery, buildListWorkflowStatesQuery, buildAddCommentMutation, buildGetCommentsQuery, buildListLabelsQuery, } from './linear.utils.js';
/**
 * Linear Service Bubble
 *
 * Agent-friendly Linear integration for issue tracking and project management.
 *
 * Core Operations:
 * - search: Find issues with text query and filters
 * - get: Get details for a specific issue
 * - create: Create new issues with markdown support
 * - update: Modify existing issues (including state changes by name)
 *
 * Supporting Operations:
 * - list_teams: List all teams
 * - list_projects: List projects (optionally by team)
 * - list_workflow_states: List workflow states for a team
 * - add_comment: Add a comment to an issue
 * - get_comments: Get comments for an issue
 * - list_labels: List labels (optionally by team)
 *
 * Features:
 * - Markdown descriptions and comments (native support)
 * - State transitions by name (auto-resolved to IDs)
 * - GraphQL API for efficient data fetching
 * - OAuth 2.0 authentication
 */
export class LinearBubble extends ServiceBubble {
    static type = 'service';
    static service = 'linear';
    static authType = 'oauth';
    static bubbleName = 'linear';
    static schema = LinearParamsSchema;
    static resultSchema = LinearResultSchema;
    static shortDescription = 'Linear integration for issue tracking and project management';
    static longDescription = `
    Agent-friendly Linear integration for issue tracking and project management.

    Core Operations:
    - search: Find issues with text query and filters
    - get: Get details for a specific issue
    - create: Create new issues with markdown support
    - update: Modify existing issues (including state changes by name)

    Supporting Operations:
    - list_teams: List all teams
    - list_projects: List projects (optionally by team)
    - list_workflow_states: List workflow states for a team
    - add_comment: Add a comment to an issue
    - get_comments: Get comments for an issue
    - list_labels: List labels (optionally by team)

    Features:
    - Markdown descriptions and comments (native Linear markdown support)
    - State transitions by name (auto-resolved to IDs)
    - GraphQL API for efficient data fetching
    - OAuth 2.0 authentication
  `;
    static alias = 'linear';
    constructor(params, context) {
        super(params, context);
    }
    async testCredential() {
        const accessToken = this.chooseCredential();
        if (!accessToken) {
            throw new Error('Linear credentials are required');
        }
        const data = await makeGraphQLRequest(accessToken, `query { viewer { id } }`);
        const viewer = data.viewer;
        if (!viewer?.id) {
            throw new Error('Linear API returned no viewer data');
        }
        return true;
    }
    /**
     * Parse the Linear credential. Unlike Jira (base64 JSON with cloudId),
     * Linear's credential is just the raw access token.
     */
    parseAccessToken() {
        const { credentials } = this.params;
        if (!credentials || typeof credentials !== 'object') {
            return null;
        }
        const linearCred = credentials[CredentialType.LINEAR_CRED];
        if (!linearCred) {
            return null;
        }
        return linearCred;
    }
    getAccessToken() {
        const token = this.parseAccessToken();
        if (!token) {
            throw new Error('Linear credentials are required. Connect your Linear account via OAuth.');
        }
        return token;
    }
    /**
     * Resolve an issue identifier (e.g., "LIN-123") to its UUID.
     * If the input is already a UUID, returns it as-is.
     */
    async resolveIssueId(identifierOrId) {
        // UUIDs are 36 chars with dashes
        if (identifierOrId.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i)) {
            return identifierOrId;
        }
        // It's an identifier like "LIN-123", look it up
        const data = await makeGraphQLRequest(this.getAccessToken(), buildGetIssueQuery(), { id: identifierOrId });
        const issue = data.issue;
        if (!issue || !issue.id) {
            throw new Error(`Issue "${identifierOrId}" not found`);
        }
        return issue.id;
    }
    /**
     * Resolve a workflow state name to its ID for a given team.
     */
    async resolveStateId(stateName, teamId) {
        const data = await makeGraphQLRequest(this.getAccessToken(), buildListWorkflowStatesQuery(), { filter: { team: { id: { eq: teamId } } } });
        const states = data.workflowStates;
        const match = states.nodes.find((s) => s.name.toLowerCase() === stateName.toLowerCase());
        if (!match) {
            const available = states.nodes.map((s) => `"${s.name}"`).join(', ');
            throw new Error(`Workflow state "${stateName}" not found. Available states: ${available}`);
        }
        return match.id;
    }
    /**
     * Get the team ID for an issue (needed for state name resolution on update).
     */
    async getIssueTeamId(issueId) {
        const data = await makeGraphQLRequest(this.getAccessToken(), `query GetIssueTeam($id: String!) { issue(id: $id) { team { id } } }`, { id: issueId });
        const issue = data.issue;
        if (!issue?.team?.id) {
            throw new Error(`Could not determine team for issue "${issueId}"`);
        }
        return issue.team.id;
    }
    async performAction(context) {
        void context;
        const { operation } = this.params;
        try {
            const result = await (async () => {
                const parsedParams = this.params;
                switch (operation) {
                    case 'search':
                        return await this.search(parsedParams);
                    case 'get':
                        return await this.get(parsedParams);
                    case 'create':
                        return await this.create(parsedParams);
                    case 'update':
                        return await this.update(parsedParams);
                    case 'list_teams':
                        return await this.listTeams(parsedParams);
                    case 'list_projects':
                        return await this.listProjects(parsedParams);
                    case 'list_workflow_states':
                        return await this.listWorkflowStates(parsedParams);
                    case 'add_comment':
                        return await this.addComment(parsedParams);
                    case 'get_comments':
                        return await this.getComments(parsedParams);
                    case 'list_labels':
                        return await this.listLabels(parsedParams);
                    default:
                        throw new Error(`Unsupported operation: ${operation}`);
                }
            })();
            return result;
        }
        catch (error) {
            return {
                operation,
                success: false,
                error: error instanceof Error ? error.message : 'Unknown error occurred',
            };
        }
    }
    // -------------------------------------------------------------------------
    // CORE OPERATION 1: search
    // -------------------------------------------------------------------------
    async search(params) {
        const { query, teamId, assigneeId, stateId, labelId, projectId, priority, limit, includeArchived, } = params;
        const accessToken = this.getAccessToken();
        // If text query is provided, use searchIssues; otherwise use issues with filters
        if (query) {
            const variables = {
                term: query,
                first: limit ?? 50,
            };
            const data = await makeGraphQLRequest(accessToken, buildSearchByTextQuery(), variables);
            const searchResult = data.searchIssues;
            return {
                operation: 'search',
                success: true,
                issues: searchResult.nodes,
                total: searchResult.totalCount ?? searchResult.nodes.length,
                error: '',
            };
        }
        else {
            // Use issues with filter
            const filter = {};
            if (teamId)
                filter.team = { id: { eq: teamId } };
            if (assigneeId)
                filter.assignee = { id: { eq: assigneeId } };
            if (stateId)
                filter.state = { id: { eq: stateId } };
            if (labelId)
                filter.labels = { some: { id: { eq: labelId } } };
            if (projectId)
                filter.project = { id: { eq: projectId } };
            if (priority !== undefined)
                filter.priority = { eq: priority };
            const data = await makeGraphQLRequest(accessToken, buildSearchQuery(), {
                filter: Object.keys(filter).length > 0 ? filter : undefined,
                first: limit ?? 50,
                includeArchived: includeArchived ?? false,
            });
            const issuesResult = data.issues;
            return {
                operation: 'search',
                success: true,
                issues: issuesResult.nodes,
                total: issuesResult.nodes.length,
                error: '',
            };
        }
    }
    // -------------------------------------------------------------------------
    // CORE OPERATION 2: get
    // -------------------------------------------------------------------------
    async get(params) {
        const { identifier } = params;
        const data = await makeGraphQLRequest(this.getAccessToken(), buildGetIssueQuery(), { id: identifier });
        return {
            operation: 'get',
            success: true,
            issue: data.issue,
            error: '',
        };
    }
    // -------------------------------------------------------------------------
    // CORE OPERATION 3: create
    // -------------------------------------------------------------------------
    async create(params) {
        const { teamId, title, description, assigneeId, priority, stateId, stateName, labelIds, projectId, dueDate, parentId, estimate, } = params;
        const input = {
            teamId,
            title,
        };
        if (description)
            input.description = description;
        if (assigneeId)
            input.assigneeId = assigneeId;
        if (priority !== undefined)
            input.priority = priority;
        if (labelIds && labelIds.length > 0)
            input.labelIds = labelIds;
        if (projectId)
            input.projectId = projectId;
        if (dueDate)
            input.dueDate = dueDate;
        if (parentId)
            input.parentId = parentId;
        if (estimate !== undefined)
            input.estimate = estimate;
        // Resolve state name to ID if provided
        if (stateName && !stateId) {
            input.stateId = await this.resolveStateId(stateName, teamId);
        }
        else if (stateId) {
            input.stateId = stateId;
        }
        const data = await makeGraphQLRequest(this.getAccessToken(), buildCreateIssueMutation(), { input });
        const result = data.issueCreate;
        if (!result.success || !result.issue) {
            throw new Error('Failed to create issue');
        }
        return {
            operation: 'create',
            success: true,
            issue: {
                id: result.issue.id,
                identifier: result.issue.identifier,
                url: result.issue.url,
            },
            error: '',
        };
    }
    // -------------------------------------------------------------------------
    // CORE OPERATION 4: update
    // -------------------------------------------------------------------------
    async update(params) {
        const { id, title, description, assigneeId, priority, stateId, stateName, labels, projectId, dueDate, estimate, } = params;
        // Resolve the issue ID (might be an identifier like "LIN-123")
        const issueId = await this.resolveIssueId(id);
        const input = {};
        if (title !== undefined)
            input.title = title;
        if (description !== undefined)
            input.description = description;
        if (assigneeId !== undefined)
            input.assigneeId = assigneeId;
        if (priority !== undefined)
            input.priority = priority;
        if (projectId !== undefined)
            input.projectId = projectId;
        if (dueDate !== undefined)
            input.dueDate = dueDate;
        if (estimate !== undefined)
            input.estimate = estimate;
        // Resolve state name to ID if provided
        if (stateName && !stateId) {
            const teamId = await this.getIssueTeamId(issueId);
            input.stateId = await this.resolveStateId(stateName, teamId);
        }
        else if (stateId) {
            input.stateId = stateId;
        }
        // Handle label modifications
        if (labels) {
            if (labels.add && labels.add.length > 0) {
                input.addLabelIds = labels.add;
            }
            if (labels.remove && labels.remove.length > 0) {
                input.removeLabelIds = labels.remove;
            }
        }
        const data = await makeGraphQLRequest(this.getAccessToken(), buildUpdateIssueMutation(), { id: issueId, input });
        const result = data.issueUpdate;
        if (!result.success || !result.issue) {
            throw new Error('Failed to update issue');
        }
        return {
            operation: 'update',
            success: true,
            issue: {
                id: result.issue.id,
                identifier: result.issue.identifier,
            },
            error: '',
        };
    }
    // -------------------------------------------------------------------------
    // SUPPORTING OPERATION: list_teams
    // -------------------------------------------------------------------------
    async listTeams(_params) {
        const data = await makeGraphQLRequest(this.getAccessToken(), buildListTeamsQuery());
        const teams = data.teams;
        return {
            operation: 'list_teams',
            success: true,
            teams: teams.nodes,
            error: '',
        };
    }
    // -------------------------------------------------------------------------
    // SUPPORTING OPERATION: list_projects
    // -------------------------------------------------------------------------
    async listProjects(params) {
        const { teamId, limit, includeArchived } = params;
        const variables = {
            first: limit ?? 50,
            includeArchived: includeArchived ?? false,
        };
        if (teamId) {
            variables.filter = {
                accessibleTeams: { some: { id: { eq: teamId } } },
            };
        }
        const data = await makeGraphQLRequest(this.getAccessToken(), buildListProjectsQuery(), variables);
        const projects = data.projects;
        return {
            operation: 'list_projects',
            success: true,
            projects: projects.nodes,
            error: '',
        };
    }
    // -------------------------------------------------------------------------
    // SUPPORTING OPERATION: list_workflow_states
    // -------------------------------------------------------------------------
    async listWorkflowStates(params) {
        const { teamId } = params;
        const data = await makeGraphQLRequest(this.getAccessToken(), buildListWorkflowStatesQuery(), { filter: { team: { id: { eq: teamId } } } });
        const states = data.workflowStates;
        return {
            operation: 'list_workflow_states',
            success: true,
            states: states.nodes,
            error: '',
        };
    }
    // -------------------------------------------------------------------------
    // SUPPORTING OPERATION: add_comment
    // -------------------------------------------------------------------------
    async addComment(params) {
        const { issueId, body } = params;
        // Resolve issue ID (might be an identifier)
        const resolvedId = await this.resolveIssueId(issueId);
        const data = await makeGraphQLRequest(this.getAccessToken(), buildAddCommentMutation(), { input: { issueId: resolvedId, body } });
        const result = data.commentCreate;
        if (!result.success || !result.comment) {
            throw new Error('Failed to add comment');
        }
        return {
            operation: 'add_comment',
            success: true,
            comment: result.comment,
            error: '',
        };
    }
    // -------------------------------------------------------------------------
    // SUPPORTING OPERATION: get_comments
    // -------------------------------------------------------------------------
    async getComments(params) {
        const { issueId, limit } = params;
        // Resolve issue ID (might be an identifier)
        const resolvedId = await this.resolveIssueId(issueId);
        const data = await makeGraphQLRequest(this.getAccessToken(), buildGetCommentsQuery(), { issueId: resolvedId, first: limit ?? 50 });
        const issue = data.issue;
        const comments = issue?.comments?.nodes ?? [];
        return {
            operation: 'get_comments',
            success: true,
            comments: comments,
            total: comments.length,
            error: '',
        };
    }
    // -------------------------------------------------------------------------
    // SUPPORTING OPERATION: list_labels
    // -------------------------------------------------------------------------
    async listLabels(params) {
        const { teamId } = params;
        const variables = {};
        if (teamId) {
            variables.filter = { team: { id: { eq: teamId } } };
        }
        const data = await makeGraphQLRequest(this.getAccessToken(), buildListLabelsQuery(), variables);
        const labels = data.issueLabels;
        return {
            operation: 'list_labels',
            success: true,
            labels: labels.nodes,
            error: '',
        };
    }
    chooseCredential() {
        const { credentials } = this.params;
        if (!credentials || typeof credentials !== 'object') {
            return undefined;
        }
        return credentials[CredentialType.LINEAR_CRED];
    }
}
//# sourceMappingURL=linear.js.map