import { CredentialType } from '@bubblelab/shared-schemas';
import { ServiceBubble } from '../../../types/service-bubble-class.js';
import { AshbyParamsSchema, AshbyResultSchema, } from './ashby.schema.js';
// Ashby API base URL
const ASHBY_API_BASE = 'https://api.ashbyhq.com';
/**
 * AshbyBubble - Integration with Ashby ATS (Applicant Tracking System)
 *
 * Provides operations for managing candidates in Ashby:
 * - List candidates with filtering
 * - Get candidate details
 * - Create new candidates
 * - Search candidates by email or name
 * - Add tags to candidates
 *
 * @example
 * ```typescript
 * // List all active candidates
 * const result = await new AshbyBubble({
 *   operation: 'list_candidates',
 *   status: 'Active',
 *   limit: 50,
 * }).action();
 *
 * // Get a specific candidate
 * const candidate = await new AshbyBubble({
 *   operation: 'get_candidate',
 *   candidate_id: 'abc123-uuid',
 * }).action();
 *
 * // Create a new candidate (Personal email becomes primary)
 * const newCandidate = await new AshbyBubble({
 *   operation: 'create_candidate',
 *   name: 'John Doe',
 *   emails: [
 *     { email: 'john.work@company.com', type: 'Work' },
 *     { email: 'john.doe@example.com', type: 'Personal' }, // This becomes primary
 *   ],
 * }).action();
 * ```
 */
export class AshbyBubble extends ServiceBubble {
    // REQUIRED: Static metadata for BubbleFactory
    static service = 'ashby';
    static authType = 'basic';
    static bubbleName = 'ashby';
    static type = 'service';
    static schema = AshbyParamsSchema;
    static resultSchema = AshbyResultSchema;
    static shortDescription = 'Ashby ATS integration for candidate management';
    static longDescription = `
    Ashby is an applicant tracking system (ATS) for modern recruiting teams.
    This bubble provides operations for:
    - Listing and filtering candidates, jobs, applications, and sources
    - Retrieving candidate, job, and application details
    - Creating and updating candidates
    - Searching candidates by email or name
    - Managing candidate tags (list, create, add to candidates)
    - Managing applications (create, change stage)
    - Adding and listing notes on candidates
    - Listing interview stages for jobs
    - Getting resume download URLs
    - Listing custom field definitions

    Security Features:
    - Uses HTTP Basic Authentication with API key
    - API key is stored encrypted and never exposed in logs

    Use Cases:
    - Sync candidates from external sources
    - Automate candidate tagging workflows
    - Build custom recruiting dashboards
    - Integrate recruiting data with other systems
    - Track application pipeline stages
    - Download candidate resumes
  `;
    static alias = 'ashby-ats';
    constructor(params = {
        operation: 'list_candidates',
        limit: 100,
    }, context) {
        super(params, context);
    }
    /**
     * Choose the appropriate credential for Ashby API
     */
    chooseCredential() {
        const params = this.params;
        const credentials = params.credentials;
        if (!credentials || typeof credentials !== 'object') {
            return undefined;
        }
        return credentials[CredentialType.ASHBY_CRED];
    }
    /**
     * Test if the credential is valid by making a simple API call
     */
    async testCredential() {
        const apiKey = this.chooseCredential();
        if (!apiKey) {
            return false;
        }
        // Use API key info endpoint to validate credential
        const response = await this.makeAshbyRequest('apiKey.info', {});
        if (response.success !== true) {
            throw new Error('Ashby API key validation failed');
        }
        return true;
    }
    /**
     * Perform the Ashby operation
     */
    async performAction(context) {
        void context; // Mark as intentionally unused
        // Cast to OUTPUT type - base class already validated and applied defaults
        const params = this.params;
        const { operation } = params;
        try {
            switch (operation) {
                case 'list_candidates':
                    return (await this.listCandidates(params));
                case 'get_candidate':
                    return (await this.getCandidate(params));
                case 'create_candidate':
                    return (await this.createCandidate(params));
                case 'search_candidates':
                    return (await this.searchCandidates(params));
                case 'add_tag':
                    return (await this.addTag(params));
                case 'list_tags':
                    return (await this.listTags(params));
                case 'create_tag':
                    return (await this.createTag(params));
                case 'list_custom_fields':
                    return (await this.listCustomFields(params));
                case 'list_jobs':
                    return (await this.listJobs(params));
                case 'get_job':
                    return (await this.getJob(params));
                case 'list_applications':
                    return (await this.listApplications(params));
                case 'get_application':
                    return (await this.getApplication(params));
                case 'create_application':
                    return (await this.createApplication(params));
                case 'change_application_stage':
                    return (await this.changeApplicationStage(params));
                case 'update_candidate':
                    return (await this.updateCandidate(params));
                case 'create_note':
                    return (await this.createNote(params));
                case 'list_notes':
                    return (await this.listNotes(params));
                case 'list_sources':
                    return (await this.listSources(params));
                case 'list_interview_stages':
                    return (await this.listInterviewStages(params));
                case 'get_file_url':
                    return (await this.getFileUrl(params));
                case 'list_projects':
                    return (await this.listProjects(params));
                case 'get_project':
                    return (await this.getProject(params));
                case 'list_candidate_projects':
                    return (await this.listCandidateProjects(params));
                case 'add_candidate_to_project':
                    return (await this.addCandidateToProject(params));
                case 'remove_candidate_from_project':
                    return (await this.removeCandidateFromProject(params));
                case 'list_project_candidate_ids':
                    return (await this.listProjectCandidateIds(params));
                case 'get_all_candidates_with_projects':
                    return (await this.getAllCandidatesWithProjects(params));
                default:
                    throw new Error(`Unknown operation: ${operation}`);
            }
        }
        catch (error) {
            const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
            return {
                operation,
                success: false,
                error: errorMessage,
            };
        }
    }
    /**
     * Extract error message from Ashby API error response
     */
    extractErrorMessage(data) {
        // Try multiple possible error locations in Ashby API responses
        // 1. Check errors array (most detailed)
        const errors = data.errors;
        if (errors && errors.length > 0 && errors[0]?.message) {
            return errors[0].message;
        }
        // 2. Check errorInfo object
        const errorInfo = data.errorInfo;
        if (errorInfo?.message) {
            return String(errorInfo.message);
        }
        // 3. Check top-level message
        if (data.message) {
            return String(data.message);
        }
        // 4. Check error field
        if (data.error) {
            return String(data.error);
        }
        // 5. Fallback: stringify the entire response for debugging
        return `Ashby API error: ${JSON.stringify(data)}`;
    }
    /**
     * Make an authenticated request to the Ashby API
     */
    async makeAshbyRequest(endpoint, body, retries = 3) {
        const apiKey = this.chooseCredential();
        if (!apiKey) {
            throw new Error('Ashby API key is required');
        }
        // Ashby uses HTTP Basic Auth with API key as username, empty password
        const authHeader = `Basic ${Buffer.from(`${apiKey}:`).toString('base64')}`;
        for (let attempt = 0; attempt <= retries; attempt++) {
            const response = await fetch(`${ASHBY_API_BASE}/${endpoint}`, {
                method: 'POST',
                headers: {
                    Authorization: authHeader,
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify(body),
            });
            // Handle rate limiting with retry
            if (response.status === 429 && attempt < retries) {
                const retryAfter = response.headers.get('retry-after');
                const waitMs = retryAfter
                    ? parseInt(retryAfter, 10) * 1000
                    : 1000 * (attempt + 1);
                await new Promise((resolve) => setTimeout(resolve, waitMs));
                continue;
            }
            let data;
            try {
                data = (await response.json());
            }
            catch {
                // Non-JSON response — retry if attempts remain
                if (attempt < retries) {
                    await new Promise((resolve) => setTimeout(resolve, 1000 * (attempt + 1)));
                    continue;
                }
                const text = await response.text().catch(() => '(unreadable)');
                throw new Error(`Failed to parse JSON from Ashby API (${endpoint}, HTTP ${response.status}): ${text.slice(0, 200)}`);
            }
            // Ashby returns success: false in the body for errors
            if (data.success === false) {
                const errorMessage = this.extractErrorMessage(data);
                throw new Error(errorMessage);
            }
            // Also check HTTP status code
            if (!response.ok) {
                const errorMessage = this.extractErrorMessage(data);
                throw new Error(`HTTP ${response.status}: ${errorMessage}`);
            }
            return data;
        }
        throw new Error(`Ashby API request failed after ${retries + 1} attempts (${endpoint})`);
    }
    /**
     * List candidates with optional filtering
     */
    async listCandidates(params) {
        const body = {};
        if (params.limit !== undefined) {
            body.limit = params.limit;
        }
        if (params.cursor) {
            body.cursor = params.cursor;
        }
        if (params.status) {
            body.status = params.status;
        }
        if (params.job_id) {
            body.jobId = params.job_id;
        }
        if (params.created_after !== undefined) {
            body.createdAfter = params.created_after;
        }
        const response = await this.makeAshbyRequest('candidate.list', body);
        return {
            operation: 'list_candidates',
            success: true,
            candidates: response.results,
            next_cursor: response.nextCursor,
            more_data_available: response.moreDataAvailable,
            sync_token: response.syncToken,
            error: '',
        };
    }
    /**
     * Get detailed information about a specific candidate
     */
    async getCandidate(params) {
        const response = await this.makeAshbyRequest('candidate.info', {
            id: params.candidate_id,
        });
        return {
            operation: 'get_candidate',
            success: true,
            candidate: response.results,
            error: '',
        };
    }
    /**
     * Normalize LinkedIn URL for comparison
     * Removes protocol, www, trailing slashes, and query params
     */
    normalizeLinkedInUrl(url) {
        try {
            // Parse URL to extract pathname
            const parsed = new URL(url);
            // Get the pathname and remove trailing slashes
            const path = parsed.pathname.replace(/\/+$/, '');
            // Normalize the path to lowercase
            return path.toLowerCase();
        }
        catch {
            // If URL parsing fails, just do basic normalization
            return url
                .toLowerCase()
                .replace(/^https?:\/\//, '')
                .replace(/^www\./, '')
                .replace(/\/+$/, '')
                .split('?')[0];
        }
    }
    /**
     * Extract a searchable name from LinkedIn URL
     * e.g., "https://linkedin.com/in/john-doe" -> "john doe"
     */
    extractNameFromLinkedInUrl(linkedinUrl) {
        try {
            const parsed = new URL(linkedinUrl);
            const pathParts = parsed.pathname.split('/').filter(Boolean);
            // LinkedIn URLs are typically /in/username or /pub/name/...
            if (pathParts.length >= 2 && pathParts[0] === 'in') {
                // Convert slug to name: "john-doe-123abc" -> "john doe"
                return pathParts[1]
                    .replace(/-[a-f0-9]{6,}$/i, '') // Remove trailing ID hash
                    .replace(/-/g, ' ')
                    .trim();
            }
            return null;
        }
        catch {
            return null;
        }
    }
    /**
     * Find existing candidates with the same LinkedIn URL
     * Uses search by name extracted from LinkedIn URL for faster lookup
     */
    async findCandidateByLinkedIn(linkedinUrl) {
        const normalizedInput = this.normalizeLinkedInUrl(linkedinUrl);
        // Extract name from LinkedIn URL for search
        const searchName = this.extractNameFromLinkedInUrl(linkedinUrl);
        if (!searchName) {
            return null;
        }
        // Search for candidates by name
        const response = await this.makeAshbyRequest('candidate.search', {
            name: searchName,
        });
        const candidates = response.results;
        // Check each candidate for matching LinkedIn URL
        for (const candidate of candidates) {
            if (candidate.socialLinks) {
                for (const link of candidate.socialLinks) {
                    if (link.type === 'LinkedIn' &&
                        this.normalizeLinkedInUrl(link.url) === normalizedInput) {
                        return {
                            id: candidate.id,
                            name: candidate.name,
                            linkedinUrl: link.url,
                        };
                    }
                }
            }
        }
        return null;
    }
    /**
     * Create a new candidate
     */
    async createCandidate(params) {
        // Check for duplicate LinkedIn profile if not allowed
        if (params.linkedin_url && !params.allow_duplicate_linkedin) {
            const existingCandidate = await this.findCandidateByLinkedIn(params.linkedin_url);
            if (existingCandidate) {
                // Return existing candidate info - not creating a new one
                const duplicateCandidate = {
                    id: existingCandidate.id,
                    name: existingCandidate.name,
                };
                return {
                    operation: 'create_candidate',
                    success: true,
                    candidate: duplicateCandidate,
                    duplicate: true,
                    error: '',
                };
            }
        }
        const body = {
            name: params.name,
        };
        if (params.emails && params.emails.length > 0) {
            // Find the personal email to use as primary
            const personalEmail = params.emails.find((e) => e.type === 'Personal');
            const otherEmails = params.emails.filter((e) => e !== personalEmail);
            if (personalEmail) {
                body.email = personalEmail.email;
                if (otherEmails.length > 0) {
                    body.alternateEmailAddresses = otherEmails.map((e) => e.email);
                }
            }
            else {
                // No personal email found, use first as primary
                body.email = params.emails[0].email;
                if (params.emails.length > 1) {
                    body.alternateEmailAddresses = params.emails
                        .slice(1)
                        .map((e) => e.email);
                }
            }
        }
        if (params.phone_number) {
            body.phoneNumber = params.phone_number;
        }
        if (params.linkedin_url) {
            body.linkedInUrl = params.linkedin_url;
        }
        if (params.github_url) {
            body.githubUrl = params.github_url;
        }
        if (params.website) {
            body.website = params.website;
        }
        if (params.source_id) {
            body.sourceId = params.source_id;
        }
        if (params.credited_to_user_id) {
            body.creditedToUserId = params.credited_to_user_id;
        }
        const response = await this.makeAshbyRequest('candidate.create', body);
        const candidate = response.results;
        // If tag is provided, add it to the newly created candidate
        if (params.tag && candidate?.id) {
            let tagId = params.tag;
            // Check if it's a UUID format (tag ID) or a tag name
            const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
            if (!uuidRegex.test(params.tag)) {
                // It's a tag name, create the tag first
                const tagResponse = await this.makeAshbyRequest('candidateTag.create', {
                    title: params.tag,
                });
                const createdTag = tagResponse.results;
                if (createdTag?.id) {
                    tagId = createdTag.id;
                }
            }
            // Add the tag to the candidate
            await this.makeAshbyRequest('candidate.addTag', {
                candidateId: candidate.id,
                tagId: tagId,
            });
        }
        return {
            operation: 'create_candidate',
            success: true,
            candidate,
            error: '',
        };
    }
    /**
     * Search for candidates by email or name
     */
    async searchCandidates(params) {
        const body = {};
        if (params.email) {
            body.email = params.email;
        }
        if (params.name) {
            body.name = params.name;
        }
        if (!params.email && !params.name) {
            throw new Error('Either email or name is required for search');
        }
        const response = await this.makeAshbyRequest('candidate.search', body);
        return {
            operation: 'search_candidates',
            success: true,
            candidates: response.results,
            error: '',
        };
    }
    /**
     * Add a tag to a candidate
     */
    async addTag(params) {
        const response = await this.makeAshbyRequest('candidate.addTag', {
            candidateId: params.candidate_id,
            tagId: params.tag_id,
        });
        return {
            operation: 'add_tag',
            success: true,
            candidate: response.results,
            error: '',
        };
    }
    /**
     * List all candidate tags
     */
    async listTags(params) {
        void params; // include_archived not used in current API
        const response = await this.makeAshbyRequest('candidateTag.list', {});
        const allTags = response.results;
        // Filter out archived tags unless include_archived is true
        const tags = params.include_archived
            ? allTags
            : allTags.filter((tag) => !tag.isArchived);
        return {
            operation: 'list_tags',
            success: true,
            tags: tags,
            error: '',
        };
    }
    /**
     * Create a new candidate tag
     */
    async createTag(params) {
        const response = await this.makeAshbyRequest('candidateTag.create', {
            title: params.title,
        });
        return {
            operation: 'create_tag',
            success: true,
            tag: response.results,
            error: '',
        };
    }
    /**
     * List all custom field definitions
     */
    async listCustomFields(params) {
        const body = {};
        if (params.limit !== undefined) {
            body.limit = params.limit;
        }
        if (params.cursor) {
            body.cursor = params.cursor;
        }
        if (params.sync_token) {
            body.syncToken = params.sync_token;
        }
        const response = await this.makeAshbyRequest('customField.list', body);
        return {
            operation: 'list_custom_fields',
            success: true,
            custom_fields: response.results,
            next_cursor: response.nextCursor,
            more_data_available: response.moreDataAvailable,
            sync_token: response.syncToken,
            error: '',
        };
    }
    /**
     * List jobs with optional filtering
     */
    async listJobs(params) {
        const body = {};
        if (params.limit !== undefined) {
            body.limit = params.limit;
        }
        if (params.cursor) {
            body.cursor = params.cursor;
        }
        if (params.status) {
            body.status = [params.status]; // Ashby API expects status as an array
        }
        const response = await this.makeAshbyRequest('job.list', body);
        return {
            operation: 'list_jobs',
            success: true,
            jobs: response.results,
            next_cursor: response.nextCursor,
            more_data_available: response.moreDataAvailable,
            error: '',
        };
    }
    /**
     * Get detailed information about a specific job
     */
    async getJob(params) {
        const response = await this.makeAshbyRequest('job.info', {
            id: params.job_id,
        });
        // Also fetch interview stages using the job's default interview plan
        let interviewStages;
        try {
            const jobData = response.results;
            const planId = jobData?.defaultInterviewPlanId;
            if (planId) {
                const stagesResponse = await this.makeAshbyRequest('interviewStage.list', { interviewPlanId: planId });
                interviewStages = stagesResponse.results;
            }
        }
        catch {
            // Interview stages fetch is optional — don't fail the whole request
            interviewStages = undefined;
        }
        return {
            operation: 'get_job',
            success: true,
            job: response.results,
            interview_stages: interviewStages,
            error: '',
        };
    }
    /**
     * List applications with optional filtering
     */
    async listApplications(params) {
        const body = {};
        if (params.limit !== undefined) {
            body.limit = params.limit;
        }
        if (params.cursor) {
            body.cursor = params.cursor;
        }
        if (params.candidate_id) {
            body.candidateId = params.candidate_id;
        }
        if (params.job_id) {
            body.jobId = params.job_id;
        }
        if (params.status) {
            body.status = params.status;
        }
        if (params.created_after !== undefined) {
            body.createdAfter = params.created_after;
        }
        const response = await this.makeAshbyRequest('application.list', body);
        return {
            operation: 'list_applications',
            success: true,
            applications: response.results,
            next_cursor: response.nextCursor,
            more_data_available: response.moreDataAvailable,
            error: '',
        };
    }
    /**
     * Get detailed information about a specific application
     */
    async getApplication(params) {
        const response = await this.makeAshbyRequest('application.info', {
            applicationId: params.application_id,
        });
        const appResult = response.results;
        return {
            operation: 'get_application',
            success: true,
            application: appResult,
            candidate: appResult?.candidate,
            job: appResult?.job,
            error: '',
        };
    }
    /**
     * Create an application (submit candidate to a job)
     */
    async createApplication(params) {
        const body = {
            candidateId: params.candidate_id,
            jobId: params.job_id,
        };
        if (params.interview_stage_id) {
            body.interviewStageId = params.interview_stage_id;
        }
        if (params.source_id) {
            body.sourceId = params.source_id;
        }
        const response = await this.makeAshbyRequest('application.create', body);
        return {
            operation: 'create_application',
            success: true,
            application: response.results,
            error: '',
        };
    }
    /**
     * Change the interview stage of an application
     */
    async changeApplicationStage(params) {
        const response = await this.makeAshbyRequest('application.changeStage', {
            applicationId: params.application_id,
            interviewStageId: params.interview_stage_id,
        });
        return {
            operation: 'change_application_stage',
            success: true,
            application: response.results,
            error: '',
        };
    }
    /**
     * Update an existing candidate
     */
    async updateCandidate(params) {
        const body = {
            id: params.candidate_id,
        };
        if (params.name !== undefined) {
            body.name = params.name;
        }
        if (params.email !== undefined) {
            body.email = params.email;
        }
        if (params.phone_number !== undefined) {
            body.phoneNumber = params.phone_number;
        }
        if (params.linkedin_url !== undefined) {
            body.linkedInUrl = params.linkedin_url;
        }
        if (params.github_url !== undefined) {
            body.githubUrl = params.github_url;
        }
        if (params.website !== undefined) {
            body.website = params.website;
        }
        const response = await this.makeAshbyRequest('candidate.update', body);
        return {
            operation: 'update_candidate',
            success: true,
            candidate: response.results,
            error: '',
        };
    }
    /**
     * Create a note on a candidate
     */
    async createNote(params) {
        const response = await this.makeAshbyRequest('candidate.createNote', {
            candidateId: params.candidate_id,
            note: params.content,
        });
        return {
            operation: 'create_note',
            success: true,
            note: response.results,
            error: '',
        };
    }
    /**
     * List notes for a candidate
     */
    async listNotes(params) {
        const response = await this.makeAshbyRequest('candidate.listNotes', {
            candidateId: params.candidate_id,
        });
        return {
            operation: 'list_notes',
            success: true,
            notes: response.results,
            error: '',
        };
    }
    /**
     * List all candidate sources
     */
    async listSources(_params) {
        const response = await this.makeAshbyRequest('source.list', {});
        return {
            operation: 'list_sources',
            success: true,
            sources: response.results,
            error: '',
        };
    }
    /**
     * List interview stages for a job (resolves the interview plan ID automatically)
     */
    async listInterviewStages(params) {
        // First get the job to find its defaultInterviewPlanId
        const jobResponse = await this.makeAshbyRequest('job.info', {
            id: params.job_id,
        });
        const jobData = jobResponse.results;
        const planId = jobData?.defaultInterviewPlanId;
        if (!planId) {
            throw new Error('Job does not have an interview plan');
        }
        const response = await this.makeAshbyRequest('interviewStage.list', {
            interviewPlanId: planId,
        });
        return {
            operation: 'list_interview_stages',
            success: true,
            interview_stages: response.results,
            error: '',
        };
    }
    /**
     * Get a download URL for a file (e.g., resume)
     */
    async getFileUrl(params) {
        const response = await this.makeAshbyRequest('file.info', {
            fileHandle: params.file_handle,
        });
        return {
            operation: 'get_file_url',
            success: true,
            file: response.results,
            error: '',
        };
    }
    /**
     * List all projects
     */
    async listProjects(_params) {
        const response = await this.makeAshbyRequest('project.list', {});
        return {
            operation: 'list_projects',
            success: true,
            projects: response.results,
            error: '',
        };
    }
    /**
     * Get project details by finding it in the project list.
     */
    async getProject(params) {
        // Use project.list and filter — project.info has unreliable parameter format
        const listResponse = await this.makeAshbyRequest('project.list', {});
        const allProjects = listResponse.results;
        const match = allProjects?.find((p) => p.id === params.project_id);
        if (!match) {
            throw new Error(`Project not found: ${params.project_id}`);
        }
        const response = { results: match };
        return {
            operation: 'get_project',
            success: true,
            project: response.results,
            error: '',
        };
    }
    /**
     * List projects a candidate belongs to
     */
    async listCandidateProjects(params) {
        const response = await this.makeAshbyRequest('candidate.listProjects', {
            candidateId: params.candidate_id,
        });
        return {
            operation: 'list_candidate_projects',
            success: true,
            projects: response.results,
            error: '',
        };
    }
    /**
     * Add a candidate to a project
     */
    async addCandidateToProject(params) {
        await this.makeAshbyRequest('candidate.addProject', {
            candidateId: params.candidate_id,
            projectId: params.project_id,
        });
        return {
            operation: 'add_candidate_to_project',
            success: true,
            error: '',
        };
    }
    /**
     * Remove a candidate from a project
     */
    async removeCandidateFromProject(params) {
        await this.makeAshbyRequest('candidate.removeProject', {
            candidateId: params.candidate_id,
            projectId: params.project_id,
        });
        return {
            operation: 'remove_candidate_from_project',
            success: true,
            error: '',
        };
    }
    /**
     * List candidate IDs belonging to a project
     */
    async listProjectCandidateIds(params) {
        const body = {
            projectId: params.project_id,
        };
        if (params.cursor) {
            body.cursor = params.cursor;
        }
        const response = await this.makeAshbyRequest('project.listCandidateIds', body);
        return {
            operation: 'list_project_candidate_ids',
            success: true,
            candidate_ids: response.results,
            next_cursor: response.nextCursor,
            more_data_available: response.moreDataAvailable,
            error: '',
        };
    }
    /**
     * Paginate all candidates, fetch all projects, then enrich each
     * candidate with their project associations via candidate.listProjects.
     *
     * Long-running operation — uses configurable concurrency with
     * Promise.allSettled for resilience against individual failures.
     */
    async getAllCandidatesWithProjects(params) {
        const concurrency = params.concurrency ?? 10;
        const t0 = Date.now();
        const elapsed = () => `${((Date.now() - t0) / 1000).toFixed(1)}s`;
        const log = (...args) => console.log(`[ashby:get_all_candidates_with_projects][${elapsed()}]`, ...args);
        // Step 1: Paginate ALL candidates
        log('starting candidate pagination...');
        const allCandidates = [];
        let cursor;
        for (let page = 0; page < 200; page++) {
            log('page', page + 1, '| total so far:', allCandidates.length);
            const body = { limit: 100 };
            if (cursor)
                body.cursor = cursor;
            const response = await this.makeAshbyRequest('candidate.list', body);
            const results = response.results;
            allCandidates.push(...(results ?? []));
            const moreDataAvailable = response.moreDataAvailable;
            const nextCursor = response.nextCursor;
            if (!moreDataAvailable || !nextCursor)
                break;
            cursor = nextCursor;
        }
        log('candidate pagination done, total:', allCandidates.length);
        // Step 2: Fetch ALL projects
        const projectResponse = await this.makeAshbyRequest('project.list', {});
        const allProjects = (projectResponse.results ?? []);
        log('fetched', allProjects.length, 'projects');
        // Step 3: Enrich each candidate with their project associations
        log('enriching candidates (concurrency:', concurrency, ')...');
        const enriched = [];
        for (let i = 0; i < allCandidates.length; i += concurrency) {
            if (i % 100 === 0) {
                log('progress:', i, '/', allCandidates.length);
            }
            const batch = allCandidates.slice(i, i + concurrency);
            const batchResults = await Promise.allSettled(batch.map(async (candidate) => {
                const candidateId = candidate.id;
                const projResponse = await this.makeAshbyRequest('candidate.listProjects', { candidateId });
                const candidateProjects = (projResponse.results ?? []);
                const socialLinks = candidate.socialLinks;
                const linkedinLink = socialLinks?.find((l) => l.type === 'LinkedIn');
                const primaryEmail = candidate.primaryEmailAddress;
                const primaryPhone = candidate.primaryPhoneNumber;
                const tags = candidate.tags;
                return {
                    id: candidateId,
                    name: candidate.name,
                    email: primaryEmail?.value ?? null,
                    phone: primaryPhone?.value ?? null,
                    linkedinUrl: linkedinLink?.url ?? null,
                    position: candidate.position ?? null,
                    company: candidate.company ?? null,
                    tags: tags?.map((t) => t.title) ?? [],
                    projectIds: candidateProjects.map((p) => p.id),
                    projectNames: candidateProjects.map((p) => p.title),
                };
            }));
            for (const result of batchResults) {
                if (result.status === 'fulfilled') {
                    enriched.push(result.value);
                }
            }
        }
        log('done:', enriched.length, '/', allCandidates.length, 'enriched');
        return {
            operation: 'get_all_candidates_with_projects',
            success: true,
            candidates: enriched,
            projects: allProjects,
            total_candidates: allCandidates.length,
            total_enriched: enriched.length,
            error: '',
        };
    }
}
//# sourceMappingURL=ashby.js.map