import { ServiceBubble } from '../../../types/service-bubble-class.js';
import { CredentialType, decodeCredentialPayload, } from '@bubblelab/shared-schemas';
import { SalesforceParamsSchema, SalesforceResultSchema, } from './salesforce.schema.js';
const SALESFORCE_API_VERSION = 'v59.0';
/**
 * Salesforce CRM Service Bubble
 *
 * Salesforce integration for querying accounts, contacts,
 * and running arbitrary SOQL queries via the Salesforce REST API.
 * Primary use case: account-level field lookup by business ID or Salesforce ID.
 */
export class SalesforceBubble extends ServiceBubble {
    static type = 'service';
    static service = 'salesforce';
    static authType = 'oauth';
    static bubbleName = 'salesforce';
    static schema = SalesforceParamsSchema;
    static resultSchema = SalesforceResultSchema;
    static shortDescription = 'Salesforce CRM integration for accounts, contacts, and SOQL queries';
    static longDescription = `
    Salesforce CRM integration for querying and searching CRM records.

    Operations:
    - get_account / get_contact: retrieve a record by Salesforce ID
    - search_accounts / search_contacts: SOQL WHERE-clause search
    - query: run arbitrary SOQL against any object
    - describe_object: list a sObject's fields with both API name AND user-facing label
    - list_objects: list all sObjects in the org with API name AND label

    IMPORTANT — label vs API name:
    Salesforce fields and objects have a backend API name (used in SOQL, e.g.
    "Treasury_Status__c") that differs from the user-facing label shown in the UI
    (e.g. "Saving Status"). Users almost always describe fields by their UI label,
    not the API name. Custom fields (suffixed __c) are especially prone to this.

    When the user references a field or object whose API name is NOT obviously a
    standard Salesforce field (e.g. Id, Name, Industry, BillingCity, Email,
    AnnualRevenue, AccountNumber, Type, OwnerId), you MUST first call
    describe_object (or list_objects for object discovery) to fetch the
    label→apiName mapping, then construct your SOQL using the apiName. Do not
    guess custom field API names from labels — call describe_object first.

    If multiple fields plausibly match the user's term, ask a clarification
    question rather than guessing.

    Result shapes:
    - describe_object → { object_name, object_label, fields: [{ apiName, label, type, custom, picklistValues?, referenceTo? }] }
    - list_objects → { objects: [{ apiName, label, custom, queryable }] }
    - get_account / get_contact → { record: {...} }
    - search_accounts / search_contacts / query → { records: [...], totalSize, done }

    Security Features:
    - OAuth 2.0 authentication with Salesforce
    - Instance-scoped access (each org has a unique API endpoint)
    - Secure credential handling with base64-encoded payloads
  `;
    static alias = '';
    constructor(params = {
        operation: 'get_account',
        record_id: '',
    }, context) {
        super(params, context);
    }
    async testCredential() {
        const creds = this.parseCredentials();
        if (!creds) {
            throw new Error('Salesforce credentials are required');
        }
        const response = await fetch(`${creds.instanceUrl}/services/data/${SALESFORCE_API_VERSION}/sobjects`, {
            headers: {
                Authorization: `Bearer ${creds.accessToken}`,
                Accept: 'application/json',
            },
        });
        if (!response.ok) {
            const text = await response.text();
            throw new Error(`Salesforce API error (${response.status}): ${text}`);
        }
        return true;
    }
    parseCredentials() {
        const { credentials } = this.params;
        if (!credentials || typeof credentials !== 'object') {
            return null;
        }
        const raw = credentials[CredentialType.SALESFORCE_CRED];
        if (!raw) {
            return null;
        }
        try {
            const parsed = decodeCredentialPayload(raw);
            if (parsed.accessToken && parsed.instanceUrl) {
                return {
                    accessToken: parsed.accessToken,
                    instanceUrl: parsed.instanceUrl.replace(/\/$/, ''),
                };
            }
        }
        catch {
            // If decoding fails, treat the raw value as an access token (validator path)
        }
        return null;
    }
    chooseCredential() {
        const creds = this.parseCredentials();
        return creds?.accessToken;
    }
    async sfRequest(endpoint, method = 'GET', body) {
        const creds = this.parseCredentials();
        if (!creds) {
            throw new Error('Salesforce credentials are required');
        }
        const url = `${creds.instanceUrl}${endpoint}`;
        const init = {
            method,
            headers: {
                Authorization: `Bearer ${creds.accessToken}`,
                'Content-Type': 'application/json',
                Accept: 'application/json',
            },
        };
        if (body && method !== 'GET') {
            init.body = JSON.stringify(body);
        }
        const response = await fetch(url, init);
        if (!response.ok) {
            const errorText = await response.text();
            throw new Error(`Salesforce API error (${response.status}): ${errorText}`);
        }
        if (response.status === 204) {
            return undefined;
        }
        return await response.json();
    }
    async performAction(context) {
        void context;
        const { operation } = this.params;
        try {
            const result = await (async () => {
                const p = this.params;
                switch (operation) {
                    case 'get_account':
                        return await this.getAccount(p);
                    case 'search_accounts':
                        return await this.searchAccounts(p);
                    case 'get_contact':
                        return await this.getContact(p);
                    case 'search_contacts':
                        return await this.searchContacts(p);
                    case 'query':
                        return await this.runQuery(p);
                    case 'describe_object':
                        return await this.describeObject(p);
                    case 'list_objects':
                        return await this.listObjects(p);
                    default:
                        throw new Error(`Unsupported operation: ${operation}`);
                }
            })();
            return result;
        }
        catch (error) {
            return {
                operation,
                success: false,
                error: error instanceof Error ? error.message : 'Unknown error occurred',
            };
        }
    }
    // ─── Operation Implementations ──────────────────────────────────────
    async getAccount(params) {
        const { record_id, fields } = params;
        let endpoint = `/services/data/${SALESFORCE_API_VERSION}/sobjects/Account/${record_id}`;
        if (fields && fields.length > 0) {
            endpoint += `?fields=${fields.join(',')}`;
        }
        const record = (await this.sfRequest(endpoint));
        return {
            operation: 'get_account',
            success: true,
            record,
            error: '',
        };
    }
    async searchAccounts(params) {
        const { where_clause, fields, limit } = params;
        const selectFields = fields && fields.length > 0
            ? fields.join(', ')
            : 'Id, Name, Industry, BillingCity, Website, Phone, AccountNumber, Type';
        const soql = `SELECT ${selectFields} FROM Account WHERE ${where_clause} LIMIT ${limit}`;
        const encoded = encodeURIComponent(soql);
        const data = (await this.sfRequest(`/services/data/${SALESFORCE_API_VERSION}/query?q=${encoded}`));
        return {
            operation: 'search_accounts',
            success: true,
            records: data.records,
            totalSize: data.totalSize,
            done: data.done,
            error: '',
        };
    }
    async getContact(params) {
        const { record_id, fields } = params;
        let endpoint = `/services/data/${SALESFORCE_API_VERSION}/sobjects/Contact/${record_id}`;
        if (fields && fields.length > 0) {
            endpoint += `?fields=${fields.join(',')}`;
        }
        const record = (await this.sfRequest(endpoint));
        return {
            operation: 'get_contact',
            success: true,
            record,
            error: '',
        };
    }
    async searchContacts(params) {
        const { where_clause, fields, limit } = params;
        const selectFields = fields && fields.length > 0
            ? fields.join(', ')
            : 'Id, FirstName, LastName, Email, Phone, AccountId, Title';
        const soql = `SELECT ${selectFields} FROM Contact WHERE ${where_clause} LIMIT ${limit}`;
        const encoded = encodeURIComponent(soql);
        const data = (await this.sfRequest(`/services/data/${SALESFORCE_API_VERSION}/query?q=${encoded}`));
        return {
            operation: 'search_contacts',
            success: true,
            records: data.records,
            totalSize: data.totalSize,
            done: data.done,
            error: '',
        };
    }
    async runQuery(params) {
        const { soql } = params;
        const encoded = encodeURIComponent(soql);
        const data = (await this.sfRequest(`/services/data/${SALESFORCE_API_VERSION}/query?q=${encoded}`));
        return {
            operation: 'query',
            success: true,
            records: data.records,
            totalSize: data.totalSize,
            done: data.done,
            error: '',
        };
    }
    async describeObject(params) {
        const { object_name } = params;
        const data = (await this.sfRequest(`/services/data/${SALESFORCE_API_VERSION}/sobjects/${encodeURIComponent(object_name)}/describe`));
        const fields = data.fields.map((f) => {
            const picklistValues = f.picklistValues
                ?.filter((p) => p.active)
                .map((p) => p.value);
            return {
                apiName: f.name,
                label: f.label,
                type: f.type,
                custom: f.custom,
                ...(picklistValues && picklistValues.length > 0
                    ? { picklistValues }
                    : {}),
                ...(f.referenceTo && f.referenceTo.length > 0
                    ? { referenceTo: f.referenceTo }
                    : {}),
            };
        });
        return {
            operation: 'describe_object',
            success: true,
            object_name: data.name,
            object_label: data.label,
            fields,
            error: '',
        };
    }
    async listObjects(params) {
        const { include_custom_only } = params;
        const data = (await this.sfRequest(`/services/data/${SALESFORCE_API_VERSION}/sobjects`));
        const objects = data.sobjects
            .filter((o) => o.queryable)
            .filter((o) => (include_custom_only ? o.custom : true))
            .map((o) => ({
            apiName: o.name,
            label: o.label,
            custom: o.custom,
            queryable: o.queryable,
        }));
        return {
            operation: 'list_objects',
            success: true,
            objects,
            error: '',
        };
    }
}
//# sourceMappingURL=salesforce.js.map