'use strict';

/**
 * Port dari packages/designer/src/migrators/field_type_resolver.rs.
 *
 * Rules engine untuk resolve tipe field frontend dari fieldValidations backend +
 * struktur JOIN datatablesQuery. Output berupa ResolvedField yang mendeskripsikan
 * tipe form input (text/number/date/select/checkbox/textarea/dll.), label,
 * posisi tabel, dan extra metadata tambahan per tipe.
 *
 * Perbedaan dengan Rust source:
 * - Rule 1a (PK skip) diperluas: SEMUA field dengan constraints.primaryKey === true
 *   akan di-skip dari output fields[] apapun tipenya (uuid, string, integer, ...).
 *   Rust source hanya skip jika type=uuid + autoGenerate. Hasil di Rust: tabel
 *   dengan PK type=string + autoGenerate (mis. visitors.visitor_id) tetap masuk
 *   ke fields[] sebagai text field. PK adalah identifier teknis, tidak perlu
 *   ditampilkan di UI form.
 */

const { generateLabel } = require('./label-generator');
const { snakeToTitle, snakeToKebab } = require('./naming');

const AUDIT_FIELDS = ['created_at', 'created_by', 'updated_at', 'updated_by'];
const TEXTAREA_FIELDS = ['address', 'description', 'notes'];
const TEXTAREA_PREFIXES = ['remark'];

// Tipe numerik kanonik RDF (lihat handbook catalogs/rdf/field-validation.md).
// Ketiganya dipetakan ke form input `number` di UDF.
const NUMERIC_TYPES = ['integer', 'decimal', 'number'];

// Mengambil nilai `constraints.default` apa adanya bila ada. Constraint `default`
// berlaku universal untuk semua tipe field (handbook field-validation.md), sehingga
// nilai default dari SDF/RDF harus terbawa ke `defaultValue` UDF.
function extractDefault(constraints) {
    return Object.prototype.hasOwnProperty.call(constraints, 'default')
        ? constraints.default
        : undefined;
}

const CHECKBOX_TEXT_DEFAULT = { checked: 'Yes', unchecked: 'No' };
const CHECKBOX_TEXT_MAP = {
    is_active: { checked: 'Active', unchecked: 'Inactive' },
    status: { checked: 'Active', unchecked: 'Inactive' }
};

const MAXLENGTH_DEFAULTS = {
    code: 20,
    name: 100,
    email: 255,
    phone: 20,
    textarea: 500,
    text: 255
};

function defaultMaxlength(fieldName) {
    if (fieldName.endsWith('_code') || fieldName === 'code') return MAXLENGTH_DEFAULTS.code;
    if (fieldName.endsWith('_name') || fieldName === 'name') return MAXLENGTH_DEFAULTS.name;
    if (fieldName.includes('email')) return MAXLENGTH_DEFAULTS.email;
    if (fieldName.includes('phone')) return MAXLENGTH_DEFAULTS.phone;
    return MAXLENGTH_DEFAULTS.text;
}

function generatePlaceholder(fieldName, label) {
    if (fieldName.includes('email')) return 'email@example.com';
    if (fieldName.includes('phone')) return '+62xxx';
    if (fieldName.endsWith('_code') || fieldName === 'code') {
        return `Enter ${label.toLowerCase()}`;
    }
    return '';
}

function guessDisplayCol(join) {
    return `${join.tableName}_name`;
}

function makeSkipped(name) {
    return { name, label: '', fieldType: '', skip: true, required: false, inTable: false, tableOrder: null, tableField: null, defaultValue: undefined, extra: {} };
}

class FieldTypeResolver {
    constructor(fieldValidations, parsedQuery, primaryKey) {
        this.parsedQuery = parsedQuery || { selectColumns: [], joins: [], mainTable: '', mainAlias: '' };
        this.primaryKey = String(primaryKey || '');

        this.validationMap = new Map();
        for (const fv of (fieldValidations || [])) {
            if (fv && typeof fv.name === 'string') {
                this.validationMap.set(fv.name, fv);
            }
        }

        this.joinMap = new Map();
        for (const join of this.parsedQuery.joins) {
            this.joinMap.set(join.localColumn, join);
        }

        const joinAliasSet = new Set(this.parsedQuery.joins.map(j => j.tableAlias));

        this.selectPositions = new Map();
        this.joinDisplayFields = new Map();
        let pkSkipped = false;
        let adjustedPos = 0;

        for (const col of this.parsedQuery.selectColumns) {
            if (joinAliasSet.has(col.tableAlias)) {
                for (const join of this.parsedQuery.joins) {
                    if (join.tableAlias === col.tableAlias) {
                        this.joinDisplayFields.set(join.localColumn, col.name);
                        adjustedPos += 1;
                        this.selectPositions.set(join.localColumn, adjustedPos);
                        break;
                    }
                }
            } else if (col.name === this.primaryKey && !pkSkipped) {
                pkSkipped = true;
                continue;
            } else {
                adjustedPos += 1;
                this.selectPositions.set(col.name, adjustedPos);
            }
        }
    }

    resolve(fieldName) {
        const validation = this.validationMap.get(fieldName) || null;
        const valType = (validation && typeof validation.type === 'string') ? validation.type : '';
        const constraints = (validation && validation.constraints && typeof validation.constraints === 'object')
            ? validation.constraints
            : {};

        // Rule 0a: Audit fields → skip
        if (AUDIT_FIELDS.includes(fieldName)) {
            return makeSkipped(fieldName);
        }

        // Rule 0b: Display-only field dari JOIN (column milik table_alias join) → skip
        for (const join of this.parsedQuery.joins) {
            if (!join.tableAlias) continue;
            const belongsToJoin = this.parsedQuery.selectColumns.some(
                col => col.tableAlias === join.tableAlias && col.name === fieldName
            );
            if (belongsToJoin) {
                return makeSkipped(fieldName);
            }
        }

        // Rule 1a (FIX): SEMUA field dengan constraints.primaryKey=true → skip
        // (extended dari Rust source yang hanya skip type=uuid + autoGenerate)
        if (constraints.primaryKey === true) {
            return makeSkipped(fieldName);
        }

        // Rule 1b: PK by name tanpa validation → skip
        if (fieldName === this.primaryKey && validation === null) {
            return makeSkipped(fieldName);
        }

        // FK + label + posisi tabel
        const isFk = this.joinMap.has(fieldName);
        const label = generateLabel(fieldName, isFk);
        const inTable = this.selectPositions.has(fieldName);
        const tableOrder = this.selectPositions.get(fieldName) || null;
        const tableField = this.joinDisplayFields.get(fieldName) || null;
        const required = constraints.required === true;

        // Rule 2: Boolean → checkbox
        if (valType === 'boolean') {
            const textMap = CHECKBOX_TEXT_MAP[fieldName] || CHECKBOX_TEXT_DEFAULT;
            const defaultVal = Object.prototype.hasOwnProperty.call(constraints, 'default')
                ? constraints.default
                : true;
            return {
                name: fieldName,
                label,
                fieldType: 'checkbox',
                skip: false,
                required: false,
                inTable,
                tableOrder,
                tableField: null,
                defaultValue: defaultVal,
                extra: {
                    checkboxText: { checked: textMap.checked, unchecked: textMap.unchecked }
                }
            };
        }

        // Rule 3: FK with JOIN → select (API)
        if (isFk) {
            const join = this.joinMap.get(fieldName);
            // Display column diambil dari kolom JOIN aktual yang dipilih di SELECT
            // (mis. category_name), bukan tebakan `<table>_name`, sehingga `select`
            // dan `tableField` konsisten.
            const displayCol = this.joinDisplayFields.get(fieldName) || guessDisplayCol(join);
            return {
                name: fieldName,
                label,
                fieldType: 'select',
                skip: false,
                required,
                inTable,
                tableOrder,
                tableField,
                defaultValue: undefined,
                extra: {
                    dataSource: {
                        type: 'api',
                        // resource = kebab-case agar cocok dengan apiPath endpoint
                        // (POST <apiBaseUrl>/<resource>/lookup). tableName SQL bersifat
                        // snake_case (visitor_categories) sehingga harus dikonversi.
                        resource: snakeToKebab(join.tableName),
                        select: [join.remoteColumn, displayCol]
                    }
                }
            };
        }

        // Rule 4: Number (integer / decimal / number)
        if (NUMERIC_TYPES.includes(valType)) {
            const extra = {};
            for (const key of ['min', 'max', 'step']) {
                if (Object.prototype.hasOwnProperty.call(constraints, key)) {
                    extra[key] = constraints[key];
                }
            }
            return {
                name: fieldName,
                label,
                fieldType: 'number',
                skip: false,
                required,
                inTable,
                tableOrder,
                tableField: null,
                defaultValue: extractDefault(constraints),
                extra
            };
        }

        // Rule 5: String with enum → select (static)
        if (valType === 'string' && Array.isArray(constraints.enum) && constraints.enum.length > 0) {
            const options = constraints.enum
                .filter(v => typeof v === 'string')
                .map(s => ({ value: s, text: snakeToTitle(s) }));
            return {
                name: fieldName,
                label,
                fieldType: 'select',
                skip: false,
                required,
                inTable,
                tableOrder,
                tableField: null,
                defaultValue: undefined,
                extra: {
                    dataSource: { type: 'static', options }
                }
            };
        }

        // Rule 6: Date
        if (fieldName.endsWith('_date') || fieldName === 'date') {
            return {
                name: fieldName,
                label,
                fieldType: 'date',
                skip: false,
                required,
                inTable,
                tableOrder,
                tableField: null,
                defaultValue: undefined,
                extra: {}
            };
        }

        // Rule 7: Time
        if (fieldName.endsWith('_time') || fieldName === 'time') {
            return {
                name: fieldName,
                label,
                fieldType: 'time',
                skip: false,
                required,
                inTable,
                tableOrder,
                tableField: null,
                defaultValue: undefined,
                extra: {}
            };
        }

        // Rule 8: Textarea
        const isTextarea = TEXTAREA_FIELDS.includes(fieldName)
            || TEXTAREA_PREFIXES.some(p => fieldName.startsWith(p));
        if (isTextarea) {
            const maxlen = (typeof constraints.maxLength === 'number')
                ? constraints.maxLength
                : MAXLENGTH_DEFAULTS.textarea;
            return {
                name: fieldName,
                label,
                fieldType: 'textarea',
                skip: false,
                required,
                inTable,
                tableOrder,
                tableField: null,
                defaultValue: extractDefault(constraints),
                extra: { rows: 3, maxlength: maxlen }
            };
        }

        // Rule 8.5: Textarea type-driven (tipe `text` first-class dari RDF).
        // Tipe `text` bersifat UNBOUNDED (phase-01 sengaja tidak menurunkan maxLength),
        // jadi `maxlength` HANYA disertakan bila constraints.maxLength ada nilainya.
        // Tidak memaksa default cap agar konsisten dengan semantik unbounded.
        if (valType === 'text') {
            const extra = { rows: 3 };
            if (typeof constraints.maxLength === 'number') {
                extra.maxlength = constraints.maxLength;
            }
            return {
                name: fieldName,
                label,
                fieldType: 'textarea',
                skip: false,
                required,
                inTable,
                tableOrder,
                tableField: null,
                defaultValue: extractDefault(constraints),
                extra
            };
        }

        // Rule 9: Default → text
        const maxlen = (typeof constraints.maxLength === 'number')
            ? constraints.maxLength
            : defaultMaxlength(fieldName);
        const extra = { maxlength: maxlen };
        const placeholder = generatePlaceholder(fieldName, label);
        if (placeholder) {
            extra.placeholder = placeholder;
        }
        return {
            name: fieldName,
            label,
            fieldType: 'text',
            skip: false,
            required,
            inTable,
            tableOrder,
            tableField: null,
            defaultValue: extractDefault(constraints),
            extra
        };
    }
}

module.exports = { FieldTypeResolver };
