import fs from 'node:fs';
import path from 'node:path';

import chalk from 'chalk';
import { resolve } from 'import-meta-resolve';

import { shouldQuote } from '@nangohq/nango-yaml';

import { TYPES_FILE_NAME } from '../constants.js';
import { printDebug } from '../utils.js';
import { NANGO_VERSION } from '../version.js';
import { parse } from './config.service.js';

import type { FunctionConfig } from '../zeroYaml/definitions.js';
import type { NangoModel, NangoModelField, NangoYamlParsed } from '@nangohq/types';

export type ModelsMap = Map<string, Record<string, any>>;

/**
 * Load nango.yaml and generate model.ts
 */
export function loadYamlAndGenerate({ fullPath, debug = false }: { fullPath: string; debug?: boolean }): NangoYamlParsed | null {
    if (debug) {
        printDebug(`Generating ${TYPES_FILE_NAME} file`);
    }

    const fp = path.resolve(fullPath, TYPES_FILE_NAME);
    if (!fs.existsSync(fp)) {
        if (debug) {
            printDebug('First compilation');
        }
    } else {
        if (debug) {
            printDebug(`File already exists, replacing`);
        }
    }

    const parsing = parse(fullPath, debug);
    if (parsing.isErr()) {
        console.log(chalk.red(parsing.error.message));
        return null;
    }

    const modelTs = buildModelsTS({ parsed: parsing.value.parsed! });
    fs.writeFileSync(fp, modelTs);

    if (debug) {
        printDebug(`${TYPES_FILE_NAME} generated`);
    }

    generateNangoJson({ parsed: parsing.value.parsed!, fullPath, debug });

    return parsing.value.parsed!;
}

/**
 * Build models.ts
 */
export function buildModelsTS({ parsed }: { parsed: NangoYamlParsed }): string {
    return `// ---------------------------
// This file was generated by Nango (v${NANGO_VERSION})
// It's recommended to version this file
// https://nango.dev
// ---------------------------

// ------ Models
${generateInterfaces({ parsed }).join('\n\n')}
// ------ /Models

// ------ SDK
${generateSDKTypes()}
// ------ /SDK

// ------ Flows
export const NangoFlows = ${JSON.stringify(parsed.integrations, null, 2)} as const;
// ------ /Flows
`;
}

export function generateInterfaces({ parsed }: { parsed: NangoYamlParsed }): string[] {
    const interfaces: string[] = [];
    for (const [, model] of parsed.models) {
        interfaces.push(modelToTypescript({ model }));
    }

    return interfaces;
}

/**
 * Transform a JSON model to its Typescript equivalent
 */
export function modelToTypescript({ model }: { model: NangoModel }) {
    const output: string[] = [];
    if (model.isAnon) {
        output.push(`export type ${model.name} = ${fieldToTypescript({ field: model.fields[0]! })}`);
    } else {
        if (model.description) {
            output.push([`/**`, ` * ${model.description}`, ` */`].join('\n'));
        }
        output.push(`export interface ${model.name} {`);
        output.push(...fieldsToTypescript({ fields: model.fields }));
        output.push(`};`);
    }
    return output.join('\n');
}

export function fieldsToTypescript({ fields }: { fields: NangoModelField[] }) {
    const output: string[] = [];
    const dynamic = fields.find((field) => field.dynamic);

    // Insert dynamic key at the beginning
    if (dynamic) {
        if (dynamic.description) {
            output.push([`  /**`, `   * ${dynamic.description}`, `   */`].join('\n'));
        }

        output.push(`  [key: string]: ${fieldToTypescript({ field: dynamic })};`);
    }

    // Regular fields
    for (const field of fields) {
        if (field.dynamic) {
            continue;
        }

        if (field.description) {
            output.push([`  /**`, `   * ${field.description}`, `   */`].join('\n'));
        }

        output.push(`  ${shouldQuote(field.name) ? `"${field.name}"` : field.name}${field.optional ? '?' : ''}: ${fieldToTypescript({ field: field })};`);
    }

    return output;
}

/**
 * Transform a field definition to its typescript equivalent
 */
export function fieldToTypescript({ field }: { field: NangoModelField }): string | boolean | null | undefined | number {
    let output = '';
    if (Array.isArray(field.value)) {
        if (field.union) {
            output = field.value.map((f) => fieldToTypescript({ field: f })).join(' | ');
        } else {
            if (!field.tsType && field.array) {
                output = field.value.map((f) => fieldToTypescript({ field: f })).join(' | ');
            } else {
                // Due to bad decision an object is an array of values without any other flag
                output = `{${fieldsToTypescript({ fields: field.value }).join('\n')}}`;
            }
            if (field.array) {
                output = `(${output})[]`;
            }
        }
    } else if (field.model || field.tsType) {
        output = `${field.value}${field.array ? '[]' : ''}`;
    } else if (field.value === null) {
        output = 'null';
    } else if (typeof field.value === 'string') {
        output = `'${field.value}${field.array ? '[]' : ''}'`;
    } else {
        output = `${field.value}${field.array ? '[]' : ''}`;
    }

    if (field.optional && !output.includes('undefined')) {
        output = `${output} | undefined`;
    }

    return output;
}

/**
 * Generate SDK types
 */
export function generateSDKTypes() {
    const filePath = resolve('@nangohq/runner-sdk/models.d.ts', import.meta.url);
    const typesContent = fs.readFileSync(new URL(filePath), 'utf8');

    return `
${typesContent}

`;
}

export function generateNangoJson({ fullPath, parsed, debug }: { fullPath: string; parsed: NangoYamlParsed; debug: boolean }): void {
    const exportPath = path.resolve(fullPath, '.nango');
    if (debug) {
        printDebug(`Generating .nango/nango.json in ${exportPath}`);
    }
    if (!fs.existsSync(exportPath)) {
        fs.mkdirSync(exportPath, { recursive: true });
    }

    const nangoJsonPath = path.join(exportPath, 'nango.json');
    fs.writeFileSync(nangoJsonPath, JSON.stringify(parsed.integrations, null, 2));
    if (debug) {
        printDebug(`Generated export ${nangoJsonPath}`);
    }
}

export function generateFunctionsJson({ fullPath, functions, debug }: { fullPath: string; functions: FunctionConfig[]; debug: boolean }): void {
    const exportPath = path.resolve(fullPath, '.nango');
    const functionsJsonPath = path.join(exportPath, 'functions.json');

    // remove any stale file when there are no functions to write
    if (functions.length === 0) {
        if (fs.existsSync(functionsJsonPath)) {
            if (debug) {
                printDebug(`Removing stale .nango/functions.json in ${exportPath}`);
            }
            fs.rmSync(functionsJsonPath);
        }
        return;
    }

    if (debug) {
        printDebug(`Generating .nango/functions.json in ${exportPath}`);
    }
    if (!fs.existsSync(exportPath)) {
        fs.mkdirSync(exportPath, { recursive: true });
    }

    fs.writeFileSync(functionsJsonPath, JSON.stringify(functions, null, 2));
    if (debug) {
        printDebug(`Generated export ${functionsJsonPath}`);
    }
}