import { SchemaValidationError } from '@takeshape/errors';
import { createAjv } from '@takeshape/json-schema';
import { mergeWithArrayMerge } from '@takeshape/util';
import isEqual from 'lodash/isEqual.js';
import isNull from 'lodash/isNull.js';
import { SERVICE_OBJECT_PATTERN_NAME } from "../constants.js";
import { createSchemaPropertyList, getGeneratorVersion, getServiceIdFieldName, getShapeDependencies, pathBetween } from "../schema-util.js";
import { latestSchemaJson } from "../schemas/index.js";
import { isEncryptedServiceConfig, isServiceAuthentication, isServiceResolver } from "../types/utils.js";
import { formatError } from "../validate/errors.js";
import { getStoredServiceConfig } from "./util.js";
/**
 * Given a `StoredServiceConfig` returns a usable `ServiceConfig` with the
 * authentication object decrypted.
 */
export function decryptServiceConfig(decryptFn, storedServiceConfig) {
    let authentication;
    if (isEncryptedServiceConfig(storedServiceConfig)) {
        const decrypted = decryptFn(storedServiceConfig.authentication);
        if (isServiceAuthentication(decrypted)) {
            authentication = decrypted;
        }
    }
    return {
        ...storedServiceConfig,
        authentication
    };
}
/**
 * Given a `ServiceMapJSON` returns a usable `ServiceMap` with all the
 * authentication objects decrypted.
 */
export async function decryptServiceMap(decryptFn, ServiceMapJSON) {
    return Object.entries(ServiceMapJSON).reduce((result, [serviceKey, serviceConfig]) => {
        return result.set(serviceKey, decryptServiceConfig(decryptFn, serviceConfig));
    }, new Map());
}
function createServiceConfigValidator() {
    const validator = createAjv();
    const { serviceConfig, ...definitions } = latestSchemaJson.definitions;
    return validator.compile({
        $id: 'ServiceConfig',
        definitions,
        ...serviceConfig
    });
}
const serviceConfigValidator = createServiceConfigValidator();
/**
 * Validates a `ServiceConfig` validator based on the JSON-schema.
 */
export function validateServiceConfig(maybeServiceConfig) {
    if (serviceConfigValidator(maybeServiceConfig)) {
        return maybeServiceConfig;
    }
    throw new SchemaValidationError('ServiceConfig was invalid', serviceConfigValidator.errors?.map(formatError));
}
/**
 * Prepare a `ServiceConfig` object update, validating the overall structure
 * and encrypting the `ServiceAuthentication` object.
 */
export function prepareServiceUpdate(encryptFn, decryptFn, projectSchema, serviceConfigUpdate, serviceKey) {
    const storedServiceConfig = getStoredServiceConfig(projectSchema, serviceKey);
    let newServiceConfig = serviceConfigUpdate;
    if (storedServiceConfig) {
        const existingServiceConfig = decryptServiceConfig(decryptFn, storedServiceConfig);
        newServiceConfig = mergeWithArrayMerge(existingServiceConfig, serviceConfigUpdate);
    }
    const { authentication, authenticationType } = newServiceConfig;
    if (authenticationType === 'none' || isNull(authentication)) {
        // biome-ignore lint/performance/noDelete: Property presence may matter here
        delete newServiceConfig.authentication;
    }
    else if (authentication !== undefined) {
        // Add the discriminator to the authentication object, set authenticationType
        // to ensure the update is correct for the auth type.
        newServiceConfig.authentication = {
            ...authentication,
            type: authenticationType
        };
    }
    validateServiceConfig(newServiceConfig);
    let serviceConfig;
    if (newServiceConfig.authentication) {
        serviceConfig = {
            ...newServiceConfig,
            authentication: encryptFn(newServiceConfig.authentication)
        };
    }
    else {
        serviceConfig = newServiceConfig;
    }
    return serviceConfig;
}
export function getServiceObjectFields(projectSchema, shape, provider) {
    return (getServiceInfo(projectSchema, shape)[provider] ?? []).filter((serviceInfo) => serviceInfo.generators[SERVICE_OBJECT_PATTERN_NAME]);
}
// Returns info for all services found in shape, keyed by provider
export function getServiceInfo(projectSchema, shape) {
    const result = {};
    // Get inline dependencies by not following refs with @resolver
    const inlineDepShapes = getShapeDependencies(projectSchema, shape, (prop) => !prop['@resolver']).map((name) => projectSchema.shapes[name]);
    const shapesToCheck = [shape, ...inlineDepShapes];
    for (const thisShape of shapesToCheck) {
        // biome-ignore lint/complexity/noForEach: Types for createSchemaPropertyList do not include the iterator
        createSchemaPropertyList(projectSchema, thisShape).forEach(([key, value]) => {
            const resolver = value['@resolver'];
            if (!isServiceResolver(resolver)) {
                return;
            }
            const serviceConfig = getStoredServiceConfig(projectSchema, resolver.service);
            if (!serviceConfig) {
                return;
            }
            const { provider } = serviceConfig;
            const generators = {};
            const tag = value['@tag'];
            if (tag?.startsWith(SERVICE_OBJECT_PATTERN_NAME)) {
                const idFieldName = getServiceIdFieldName(key);
                const version = tag.substr(Number(tag.lastIndexOf(':')) + 1);
                generators[SERVICE_OBJECT_PATTERN_NAME] = {
                    version,
                    currentVersion: getGeneratorVersion(SERVICE_OBJECT_PATTERN_NAME),
                    idFieldName,
                    serviceObjectType: resolver.fieldName
                };
            }
            const serviceInfo = {
                service: resolver.service,
                fieldName: key,
                path: pathBetween(shape, thisShape, projectSchema),
                generators
            };
            if (Array.isArray(result[provider])) {
                // Dedupe service info
                if (!result[provider].find((existingServiceInfo) => isEqual(existingServiceInfo, serviceInfo))) {
                    result[provider].push(serviceInfo);
                }
            }
            else {
                result[provider] = [serviceInfo];
            }
        });
    }
    return result;
}
