import { DepGraph } from 'dependency-graph';
import { getCategoriesFromResource, getSpotlightConfigurationForResource, } from '../compiler/lightdashProjectConfig';
import assertUnreachable from '../utils/assertUnreachable';
import { getItemId } from '../utils/item';
import { ParseError } from './errors';
import { FieldType, friendlyName, getMinMaxBaseDimensionMetadata, } from './field';
import { parseFilters } from './filterGrammar';
export var SupportedDbtAdapter;
(function (SupportedDbtAdapter) {
    SupportedDbtAdapter["BIGQUERY"] = "bigquery";
    SupportedDbtAdapter["DATABRICKS"] = "databricks";
    SupportedDbtAdapter["SNOWFLAKE"] = "snowflake";
    SupportedDbtAdapter["REDSHIFT"] = "redshift";
    SupportedDbtAdapter["POSTGRES"] = "postgres";
    SupportedDbtAdapter["DUCKDB"] = "duckdb";
    SupportedDbtAdapter["TRINO"] = "trino";
    SupportedDbtAdapter["CLICKHOUSE"] = "clickhouse";
    SupportedDbtAdapter["ATHENA"] = "athena";
    SupportedDbtAdapter["SPARK"] = "spark";
})(SupportedDbtAdapter || (SupportedDbtAdapter = {}));
export const normaliseModelDatabase = (model, targetWarehouse) => {
    switch (targetWarehouse) {
        case SupportedDbtAdapter.POSTGRES:
        case SupportedDbtAdapter.BIGQUERY:
        case SupportedDbtAdapter.SNOWFLAKE:
        case SupportedDbtAdapter.TRINO:
        case SupportedDbtAdapter.ATHENA:
        case SupportedDbtAdapter.REDSHIFT:
        case SupportedDbtAdapter.DUCKDB:
            if (model.database === null) {
                throw new ParseError(`Cannot parse dbt model '${model.unique_id}' because the database field has null value.`, {});
            }
            return { ...model, database: model.database };
        case SupportedDbtAdapter.CLICKHOUSE:
            return { ...model, database: '' }; // Clickhouse doesn't have a database field
        case SupportedDbtAdapter.DATABRICKS:
        case SupportedDbtAdapter.SPARK:
            return { ...model, database: model.database || 'DEFAULT' };
        default:
            return assertUnreachable(targetWarehouse, `Cannot recognise warehouse ${targetWarehouse}`);
    }
};
export const patchPathParts = (patchPath) => {
    // dbt-core format: `project://path/to/file.yml`
    // dbt-fusion format: raw path with no project prefix, may contain Windows backslashes
    const normalized = patchPath.replace(/\\/g, '/');
    const separatorIndex = normalized.indexOf('://');
    if (separatorIndex === -1) {
        return {
            project: null,
            path: normalized,
        };
    }
    return {
        project: normalized.slice(0, separatorIndex),
        path: normalized.slice(separatorIndex + '://'.length),
    };
};
export const buildModelGraph = (allModels) => {
    const depGraph = new DepGraph();
    const lookup = Object.fromEntries(allModels.map((model) => [model.unique_id, model]));
    allModels.forEach((model) => {
        depGraph.addNode(model.unique_id, { type: 'model', name: model.name });
        // Only use models for graph.
        model.depends_on?.nodes?.forEach((nodeId) => {
            const node = lookup[nodeId];
            if (node) {
                depGraph.addNode(node.unique_id, {
                    type: 'model',
                    name: node.name,
                });
                depGraph.addDependency(model.unique_id, node.unique_id);
            }
        });
    });
    return depGraph;
};
export const isDbtRpcDocsGenerateResults = (results) => 'nodes' in results &&
    typeof results.nodes === 'object' &&
    results.nodes !== null &&
    Object.values(results.nodes).every((node) => typeof node === 'object' &&
        node !== null &&
        'metadata' in node &&
        'columns' in node);
export const isDbtPackages = (results) => 'packages' in results;
const isDbtRawManifestMetadata = (x) => typeof x === 'object' &&
    x !== null &&
    'dbt_schema_version' in x &&
    'generated_at' in x &&
    'adapter_type' in x;
export const isSupportedDbtAdapter = (x) => isDbtRawManifestMetadata(x) &&
    Object.values(SupportedDbtAdapter).includes(x.adapter_type);
export const isSupportedDbtAdapterType = (x) => Object.values(SupportedDbtAdapter).includes(x);
export const isDbtRpcManifestResults = (results) => 'manifest' in results &&
    typeof results.manifest === 'object' &&
    results.manifest !== null &&
    'nodes' in results.manifest &&
    'metadata' in results.manifest &&
    'metrics' in results.manifest &&
    isDbtRawManifestMetadata(results.manifest.metadata);
export const isDbtRpcCompileResults = (results) => 'results' in results &&
    Array.isArray(results.results) &&
    results.results.every((result) => typeof result === 'object' &&
        result !== null &&
        'node' in result &&
        typeof result.node === 'object' &&
        result.node !== null &&
        'unique_id' in result.node &&
        'resource_type' in result.node);
export const convertToGroups = (dbtGroups, dbtGroupLabel) => {
    let groups = [];
    if (dbtGroups) {
        if (typeof dbtGroups === 'string') {
            groups = [dbtGroups];
        }
        else {
            groups = [...dbtGroups];
        }
    }
    else if (dbtGroupLabel) {
        groups = [dbtGroupLabel];
    }
    return groups;
};
export const convertToAiHints = (aiHint) => {
    if (!aiHint) {
        return undefined;
    }
    if (typeof aiHint === 'string') {
        return [aiHint];
    }
    return aiHint;
};
export const isDbtRpcRunSqlResults = (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
results) => 'results' in results &&
    Array.isArray(results.results) &&
    results.results.every((result) => typeof result === 'object' &&
        result !== null &&
        'table' in result &&
        typeof result.table === 'object' &&
        result.table !== null &&
        'column_names' in result.table &&
        Array.isArray(result.table.column_names) &&
        'rows' in result.table &&
        Array.isArray(result.table.rows));
export const convertModelMetric = ({ modelName, name, metric, source, tableLabel, dimensionReference, requiredAttributes, anyAttributes, spotlightConfig, modelCategories = [], modelOwner, defaultShowUnderlyingValues, baseDimensionType, baseDimensionTimeInterval, }) => {
    const groups = convertToGroups(metric.groups, metric.group_label);
    const spotlightVisibility = metric.spotlight?.visibility ?? spotlightConfig?.default_visibility;
    const metricCategories = Array.from(new Set([...modelCategories, ...(metric.spotlight?.categories || [])]));
    const spotlightCategories = getCategoriesFromResource('metric', name, spotlightConfig, metricCategories);
    // Metric owner takes precedence over model owner
    const owner = metric.spotlight?.owner ?? modelOwner;
    const [parsedDefaultFilter] = parseFilters(metric.spotlight?.default_filter
        ? [metric.spotlight.default_filter]
        : undefined);
    // A spotlight default filter is pre-applied but removable, i.e. non-required.
    const defaultFilter = parsedDefaultFilter
        ? { ...parsedDefaultFilter, required: false }
        : undefined;
    return {
        fieldType: FieldType.METRIC,
        name,
        label: metric.label || friendlyName(name),
        sql: metric.sql,
        table: modelName,
        tableLabel,
        type: metric.type,
        description: metric.description,
        source,
        hidden: !!metric.hidden,
        round: metric.round,
        compact: metric.compact,
        format: metric.format,
        separator: metric.separator,
        groups,
        showUnderlyingValues: metric.show_underlying_values ?? defaultShowUnderlyingValues,
        filters: parseFilters(metric.filters),
        percentile: metric.percentile,
        ...(metric.distinct_keys
            ? {
                distinctKeys: Array.isArray(metric.distinct_keys)
                    ? metric.distinct_keys
                    : [metric.distinct_keys],
            }
            : {}),
        dimensionReference,
        ...getMinMaxBaseDimensionMetadata(metric.type, baseDimensionType
            ? {
                type: baseDimensionType,
                timeInterval: baseDimensionTimeInterval,
            }
            : undefined),
        requiredAttributes,
        anyAttributes,
        ...(metric.urls ? { urls: metric.urls } : null),
        ...(metric.tags
            ? {
                tags: Array.isArray(metric.tags)
                    ? metric.tags
                    : [metric.tags],
            }
            : null),
        ...(metric.default_time_dimension
            ? {
                defaultTimeDimension: {
                    field: metric.default_time_dimension.field,
                    interval: metric.default_time_dimension.interval,
                },
            }
            : null),
        ...getSpotlightConfigurationForResource({
            visibility: spotlightVisibility,
            categories: spotlightCategories,
            filterBy: metric.spotlight?.filter_by,
            segmentBy: metric.spotlight?.segment_by,
            defaultSegment: metric.spotlight?.default_segment,
            defaultFilter,
            owner,
        }),
        ...(metric.drivers ? { drivers: metric.drivers } : {}),
        ...(metric.ai_hint ? { aiHint: convertToAiHints(metric.ai_hint) } : {}),
        ...(metric.richText ? { richText: metric.richText } : {}),
    };
};
export const convertColumnMetric = ({ modelName, dimensionName, dimensionSql, dimensionType, dimensionTimeInterval, name, metric, source, tableLabel, requiredAttributes, anyAttributes, spotlightConfig, modelCategories = [], modelOwner, defaultShowUnderlyingValues, }) => {
    // The column's type is only a reliable base when the metric actually
    // aggregates it: no `sql`, or a `sql` equal to dimensionSql. A `sql` pointing
    // elsewhere (e.g. MAX(id) under a DATE column) must not inherit the type.
    const aggregatesOwnColumn = !metric.sql || metric.sql === dimensionSql;
    return convertModelMetric({
        modelName,
        name,
        metric: {
            ...metric,
            sql: metric.sql || dimensionSql,
            description: metric.description ||
                (dimensionName
                    ? `${friendlyName(metric.type)} of ${friendlyName(dimensionName)}`
                    : undefined),
        },
        source,
        tableLabel,
        dimensionReference: dimensionName
            ? getItemId({ table: modelName, name: dimensionName })
            : undefined,
        baseDimensionType: aggregatesOwnColumn ? dimensionType : undefined,
        baseDimensionTimeInterval: aggregatesOwnColumn
            ? dimensionTimeInterval
            : undefined,
        requiredAttributes,
        anyAttributes,
        ...(metric.default_time_dimension
            ? {
                defaultTimeDimension: {
                    field: metric.default_time_dimension.field,
                    interval: metric.default_time_dimension.interval,
                },
            }
            : null),
        spotlightConfig,
        modelCategories,
        modelOwner,
        defaultShowUnderlyingValues,
    });
};
export var DbtManifestVersion;
(function (DbtManifestVersion) {
    DbtManifestVersion["V7"] = "v7";
    DbtManifestVersion["V8"] = "v8";
    DbtManifestVersion["V9"] = "v9";
    DbtManifestVersion["V10"] = "v10";
    DbtManifestVersion["V11"] = "v11";
    DbtManifestVersion["V12"] = "v12";
    // dbt fusion
    DbtManifestVersion["V20"] = "v20";
})(DbtManifestVersion || (DbtManifestVersion = {}));
export const getDbtManifestVersion = (manifest) => {
    const version = manifest.metadata.dbt_schema_version.match(/\/(v\d+).json/)?.[1];
    if (!version) {
        throw new Error(`Could not determine dbt manifest version from ${manifest.metadata.dbt_schema_version}`);
    }
    if (Object.values(DbtManifestVersion).includes(version)) {
        return version;
    }
    throw new Error(`Unsupported dbt manifest version: ${version}`);
};
export const getLatestSupportedDbtManifestVersion = () => {
    const versions = Object.values(DbtManifestVersion);
    return versions[versions.length - 1];
};
export var DbtExposureType;
(function (DbtExposureType) {
    DbtExposureType["DASHBOARD"] = "dashboard";
    DbtExposureType["NOTEBOOK"] = "notebook";
    DbtExposureType["ANALYSIS"] = "analysis";
    DbtExposureType["ML"] = "ml";
    DbtExposureType["APPLICATION"] = "application";
})(DbtExposureType || (DbtExposureType = {}));
export const getModelsFromManifest = (manifest) => {
    const models = Object.values(manifest.nodes).filter((node) => ['model', 'seed'].includes(node.resource_type) &&
        node.config?.materialized !== 'ephemeral');
    if (!isSupportedDbtAdapter(manifest.metadata)) {
        throw new ParseError(`dbt adapter not supported. Lightdash does not support adapter ${manifest.metadata.adapter_type}`, {});
    }
    const adapterType = manifest.metadata.adapter_type;
    return models.map((model) => normaliseModelDatabase(model, adapterType));
};
export function getCompiledModels(manifestModels, compiledModelIds) {
    const isAnyModelCompiled = manifestModels.some((model) => model.compiled);
    return manifestModels.filter((model) => {
        // Seeds are always included — they don't appear in dbt ls output
        // and don't have a compiled field.
        if (model.resource_type === 'seed')
            return true;
        if (compiledModelIds) {
            return compiledModelIds.includes(model.unique_id);
        }
        // In case any model is compiled, we only return the compiled models otherwise we return all models (this maintains backwards compatibility + adds ability to use parse)
        return isAnyModelCompiled ? model.compiled : true;
    });
}
//# sourceMappingURL=dbt.js.map