'use strict';

/**
 * Migrate Runner — orchestrator untuk command `payload migrate`.
 *
 * Tanggung jawab:
 *  1. Resolve file config (.env) via cascade lookup standar platform
 *  2. Baca SERVER_ADDRESS dan SERVER_PORT dari config → konstruksi apiBaseUrl
 *     dengan format `http://{host}:{port}/api/{project}`
 *  3. Load file payload backend (RDF) dari --name (relative ke cwd/payload/
 *     atau absolute) dan resolve referensi `file:` pada datatablesQuery/viewQuery
 *  4. Discovery RDF terkait via JOIN di datatablesQuery: setiap tabel yang
 *     di-JOIN dimuat RDF-nya dari folder yang sama lalu dikonversi menjadi page
 *     tersendiri (1 RDF utama → multi page)
 *  5. Jalankan migrasi RDF → UDF via BackendPayloadMigrator
 *  6. Tulis output dalam format SPLIT multi-file:
 *       <output>/app-config.json          → { appConfig }
 *       <output>/pages/<pageId>.json       → { pages: [ <page> ] }
 *       <output>/<appCode>.json            → aggregator (extends/homepage/include/navigation)
 */

const fs = require('fs');
const path = require('path');

const { resolveConfig, printDefaultConfigWarning } = require('../utils/config-resolver');
const { readEnvFile } = require('../utils/env-manager');
const { migrate } = require('./backend-payload-migrator');
const { parseDatatablesQuery } = require('./sql-parser');
const { kebabToTitle, snakeToKebab } = require('./naming');

const DEFAULT_APP_NAME = 'My Application';
const DEFAULT_APP_CODE_FALLBACK = 'my-app';
const DEFAULT_PLUGIN = 'vanilla-js-basic';
const DEFAULT_HOST = '127.0.0.1';
const DEFAULT_BACKEND_PORT = 3000;
const DEFAULT_FRONTEND_PORT = 8000;

const QUERY_REF_FIELDS = ['datatablesQuery', 'viewQuery'];

function resolveInputPath(nameArg, cwd) {
    if (path.isAbsolute(nameArg)) return nameArg;
    const direct = path.resolve(cwd, nameArg);
    if (fs.existsSync(direct)) return direct;
    const inPayload = path.resolve(cwd, 'payload', nameArg);
    if (fs.existsSync(inPayload)) return inPayload;
    // Fallback: return cwd-resolved path so caller dapat tampilkan error path
    return direct;
}

function buildOutputDir(outputArg, cwd) {
    if (!outputArg) {
        return path.resolve(cwd, 'frontend', 'payload');
    }
    const abs = path.isAbsolute(outputArg) ? outputArg : path.resolve(cwd, outputArg);
    // Toleransi bila --output diakhiri .json: pakai direktori induknya sebagai
    // root output split (output bukan file tunggal lagi).
    if (/\.json$/i.test(abs)) return path.dirname(abs);
    return abs;
}

function readServerConfig(envFilePath) {
    const { data } = readEnvFile(envFilePath);
    const host = (typeof data.SERVER_ADDRESS === 'string' && data.SERVER_ADDRESS.length > 0)
        ? data.SERVER_ADDRESS
        : DEFAULT_HOST;
    const portRaw = (typeof data.SERVER_PORT === 'string' && data.SERVER_PORT.length > 0)
        ? data.SERVER_PORT
        : '';
    const portNum = parseInt(portRaw, 10);
    const port = Number.isFinite(portNum) && portNum > 0 ? portNum : DEFAULT_BACKEND_PORT;
    return { host, port };
}

function buildApiBaseUrl(host, port, project) {
    const projectSegment = project ? `/${project}` : '';
    return `http://${host}:${port}/api${projectSegment}`;
}

/**
 * Gabungkan aggregator `<project>.json` yang sudah ada dengan hasil run saat ini.
 * Karena nama file aggregator = nama project, run berikutnya untuk project yang
 * sama akan menambahkan (akumulasi) page baru, bukan menimpa.
 *
 * Aturan merge:
 *  - `pages[]`        : union berdasarkan path `include` (entry existing didahulukan,
 *                       page baru di-append). Re-migrate page yang sama = idempoten.
 *  - `navigation.items`: union berdasarkan `pageRef` (type=page). Item existing
 *                       dipertahankan sehingga kustomisasi manual (mis. icon/label)
 *                       pada page lama tidak hilang.
 *  - `homepage`       : dipertahankan dari file existing (run pertama yang menetapkan).
 *  - `extends`        : dipertahankan.
 */
function mergeAggregator(existing, fresh) {
    if (!existing || typeof existing !== 'object' || Array.isArray(existing)) {
        return fresh;
    }

    const seenInclude = new Set();
    const mergedPages = [];
    const pushPages = (list) => {
        if (!Array.isArray(list)) return;
        for (const entry of list) {
            const key = (entry && typeof entry.include === 'string')
                ? entry.include
                : JSON.stringify(entry);
            if (seenInclude.has(key)) continue;
            seenInclude.add(key);
            mergedPages.push(entry);
        }
    };
    pushPages(existing.pages);
    pushPages(fresh.pages);

    const seenNav = new Set();
    const mergedNav = [];
    const pushNav = (items) => {
        if (!Array.isArray(items)) return;
        for (const item of items) {
            const key = (item && item.type === 'page' && item.pageRef)
                ? `page:${item.pageRef}`
                : JSON.stringify(item);
            if (seenNav.has(key)) continue;
            seenNav.add(key);
            mergedNav.push(item);
        }
    };
    const existingNav = (existing.navigation && Array.isArray(existing.navigation.items))
        ? existing.navigation.items : [];
    const freshNav = (fresh.navigation && Array.isArray(fresh.navigation.items))
        ? fresh.navigation.items : [];
    pushNav(existingNav);
    pushNav(freshNav);

    return {
        extends: existing.extends || fresh.extends,
        homepage: (typeof existing.homepage === 'string' && existing.homepage)
            ? existing.homepage
            : fresh.homepage,
        pages: mergedPages,
        navigation: { items: mergedNav }
    };
}

/**
 * Resolve referensi `file:` pada query fields (datatablesQuery, viewQuery)
 * menjadi isi SQL inline. Dibaca langsung dari disk relatif terhadap lokasi
 * file RDF, independen dari mode pkg (file SQL ada di project user, bukan
 * di-bundle ke binary). Bila file tidak ditemukan, query di-kosongkan dan
 * sebuah warning dikumpulkan agar parsing JOIN tidak crash.
 */
function resolveQueryRefs(payload, payloadPath, warnings) {
    const dir = path.dirname(payloadPath);
    const out = Object.assign({}, payload);
    for (const key of QUERY_REF_FIELDS) {
        const val = out[key];
        if (typeof val === 'string' && val.startsWith('file:')) {
            const rel = val.slice(5);
            const full = path.isAbsolute(rel) ? rel : path.resolve(dir, rel);
            if (fs.existsSync(full)) {
                out[key] = fs.readFileSync(full, 'utf8');
            } else {
                out[key] = '';
                warnings.push(`${key} reference not found: ${full} (referenced by ${path.basename(payloadPath)})`);
            }
        }
    }
    return out;
}

function loadRdf(filePath) {
    const content = fs.readFileSync(filePath, 'utf8');
    return JSON.parse(content);
}

/**
 * Petakan nama tabel JOIN (snake_case, mis. visitor_categories) ke file RDF
 * sibling di folder payload. Coba bentuk kebab dulu (visitor-categories.json),
 * lalu bentuk snake (visitor_categories.json).
 */
function findRelatedRdfPath(tableName, payloadDir) {
    const candidates = [`${snakeToKebab(tableName)}.json`, `${tableName}.json`];
    for (const c of candidates) {
        const p = path.join(payloadDir, c);
        if (fs.existsSync(p)) return { path: p, candidates };
    }
    return { path: null, candidates };
}

async function run(args) {
    const cwd = process.cwd();

    if (!args.name || typeof args.name !== 'string') {
        const err = new Error('Flag --name is required (backend payload file name, e.g. visitors.json)');
        err.exitCode = 2;
        throw err;
    }
    if (!args.project || typeof args.project !== 'string') {
        const err = new Error('Flag --project is required (project name used in apiBaseUrl)');
        err.exitCode = 2;
        throw err;
    }

    const resolved = resolveConfig(args.config, cwd);
    if (!resolved) {
        const err = new Error('Flag --config is required, or set a default config via `restforge config set-default`');
        err.exitCode = 2;
        throw err;
    }
    if (resolved.source === 'default') {
        printDefaultConfigWarning(resolved.defaultName);
    }
    if (!fs.existsSync(resolved.path)) {
        const err = new Error(`Config file not found: ${resolved.path}`);
        err.exitCode = 3;
        throw err;
    }

    const { host, port: backendPort } = readServerConfig(resolved.path);
    const project = args.project;
    const apiBaseUrl = buildApiBaseUrl(host, backendPort, project);

    const frontendPortArg = (typeof args.port === 'number' && Number.isFinite(args.port) && args.port > 0)
        ? args.port
        : DEFAULT_FRONTEND_PORT;

    const appName = args.appName || kebabToTitle(project) || DEFAULT_APP_NAME;
    const appCode = args.appCode || project || DEFAULT_APP_CODE_FALLBACK;
    const plugin = args.plugin || DEFAULT_PLUGIN;

    const inputPath = resolveInputPath(args.name, cwd);
    if (!fs.existsSync(inputPath)) {
        const err = new Error(`Input payload file not found: ${inputPath}`);
        err.exitCode = 3;
        throw err;
    }

    const warnings = [];

    let mainPayloadRaw;
    try {
        mainPayloadRaw = loadRdf(inputPath);
    } catch (e) {
        const err = new Error(`Failed to parse JSON from ${inputPath}: ${e.message}`);
        err.exitCode = 3;
        throw err;
    }

    // Resolve file: refs pada RDF utama agar JOIN dapat di-parse
    const mainPayload = resolveQueryRefs(mainPayloadRaw, inputPath, warnings);
    const mainTableClean = String(mainPayload.tableName || '').split('.').pop() || '';

    // Discovery tabel terkait via JOIN (1 level dari RDF utama)
    const payloadDir = path.dirname(inputPath);
    const parsedMain = parseDatatablesQuery(mainPayload.datatablesQuery || '');
    const relatedPayloads = [];
    const seenTables = new Set([mainTableClean]);
    for (const join of parsedMain.joins) {
        const t = String(join.tableName || '').split('.').pop() || '';
        if (!t || seenTables.has(t)) continue;
        seenTables.add(t);

        const { path: relPath, candidates } = findRelatedRdfPath(t, payloadDir);
        if (!relPath) {
            warnings.push(`Related table '${t}' referenced by JOIN but no RDF file found in ${payloadDir} (tried: ${candidates.join(', ')}); page not generated, only the select reference is kept`);
            continue;
        }
        try {
            const relRaw = loadRdf(relPath);
            relatedPayloads.push(resolveQueryRefs(relRaw, relPath, warnings));
        } catch (e) {
            warnings.push(`Failed to parse related RDF ${relPath}: ${e.message}`);
        }
    }

    // Urutan page: tabel terkait (master) lebih dulu, RDF utama (detail) terakhir
    const orderedPayloads = relatedPayloads.concat([mainPayload]);

    const result = migrate(orderedPayloads, appName, appCode, plugin, apiBaseUrl, frontendPortArg);
    if (!result.success) {
        const msg = (result.errors && result.errors.length > 0)
            ? result.errors.join('; ')
            : 'Migration failed without details';
        const err = new Error(msg);
        err.exitCode = 1;
        throw err;
    }
    for (const w of (result.warnings || [])) warnings.push(w);

    const pages = result.payload.pages;
    const appConfig = result.payload.appConfig;
    // homepage = page dari RDF utama (entry terakhir karena di-append paling akhir)
    const mainPageId = pages.length > 0 ? pages[pages.length - 1].pageId : '';

    const outputDir = buildOutputDir(args.output, cwd);

    // Susun semua file target untuk format split
    const aggregatorPath = path.join(outputDir, `${appCode}.json`);

    // File app-config + per-page: tunduk pada gate overwrite.
    const pageTargets = [];
    pageTargets.push({
        path: path.join(outputDir, 'app-config.json'),
        content: { appConfig }
    });
    for (const page of pages) {
        pageTargets.push({
            path: path.join(outputDir, 'pages', `${page.pageId}.json`),
            content: { pages: [page] }
        });
    }

    // Gate overwrite hanya untuk app-config + pages. Aggregator TIDAK dihitung
    // sebagai konflik karena ia di-merge (akumulasi), bukan ditimpa.
    if (!args.overwrite) {
        const existing = pageTargets.filter(t => fs.existsSync(t.path)).map(t => t.path);
        if (existing.length > 0) {
            const err = new Error(`Output file(s) already exist:\n  ${existing.join('\n  ')}\nUse --overwrite to replace.`);
            err.exitCode = 1;
            throw err;
        }
    }

    // Aggregator: gabungkan dengan file existing bila project sama (nama file sama)
    const freshAggregator = {
        extends: 'app-config.json',
        homepage: mainPageId,
        pages: pages.map(p => ({ include: `pages/${p.pageId}.json` })),
        navigation: {
            items: pages.map(p => ({ type: 'page', pageRef: p.pageId, label: p.pageTitle }))
        }
    };
    let aggregatorMerged = false;
    let aggregatorContent = freshAggregator;
    if (fs.existsSync(aggregatorPath)) {
        try {
            const existingAgg = JSON.parse(fs.readFileSync(aggregatorPath, 'utf8'));
            aggregatorContent = mergeAggregator(existingAgg, freshAggregator);
            aggregatorMerged = true;
        } catch (e) {
            warnings.push(`Existing aggregator ${path.basename(aggregatorPath)} could not be parsed (${e.message}); it will be replaced`);
        }
    }

    const targets = pageTargets.concat([{ path: aggregatorPath, content: aggregatorContent }]);
    for (const t of targets) {
        const dir = path.dirname(t.path);
        if (!fs.existsSync(dir)) {
            fs.mkdirSync(dir, { recursive: true });
        }
        fs.writeFileSync(t.path, JSON.stringify(t.content, null, 2), 'utf8');
    }

    const stdout = process.stdout;
    stdout.write('============================================================\n');
    stdout.write('PAYLOAD MIGRATE - RDF (backend) -> UDF (frontend, split)\n');
    stdout.write('============================================================\n\n');
    stdout.write(`  Input        : ${inputPath}\n`);
    stdout.write(`  Output dir   : ${outputDir}\n`);
    stdout.write(`  Project      : ${project}\n`);
    stdout.write(`  apiBaseUrl   : ${apiBaseUrl}\n`);
    stdout.write(`  Backend port : ${backendPort}\n`);
    stdout.write(`  Frontend port: ${frontendPortArg}\n`);
    stdout.write(`  Homepage     : ${aggregatorContent.homepage}\n`);
    stdout.write(`  Pages (run)  : ${result.pageResults.length}\n`);
    stdout.write(`  Pages (app)  : ${aggregatorContent.pages.length}${aggregatorMerged ? ' (merged with existing)' : ''}\n\n`);
    for (const pr of result.pageResults) {
        stdout.write(`  [OK] ${pr.pageId}: ${pr.fieldCount} field(s), ${pr.tableColCount} table column(s)\n`);
    }
    stdout.write('\n  Files written:\n');
    for (const t of targets) {
        const rel = path.relative(outputDir, t.path) || path.basename(t.path);
        const tag = (t.path === aggregatorPath && aggregatorMerged) ? '  (merged)' : '';
        stdout.write(`    - ${rel}${tag}\n`);
    }
    if (warnings.length > 0) {
        stdout.write('\n  Warnings:\n');
        for (const w of warnings) {
            stdout.write(`    - ${w}\n`);
        }
    }
    stdout.write('\n  Migration completed successfully.\n');
}

module.exports = {
    run,
    resolveInputPath,
    buildOutputDir,
    resolveQueryRefs,
    findRelatedRdfPath,
    readServerConfig,
    buildApiBaseUrl,
    mergeAggregator
};
