'use strict';

/**
 * Schema Printer — serialize IR object to factory function source code.
 *
 * Inverse of dbschema-kit's loader+ir-builder pipeline. Accepts the IR shape
 * produced by `defineModel()` (or `mapTableMetaToIR()`) and emits a string
 * containing a valid factory function that, when re-loaded, yields an
 * equivalent IR.
 *
 * @module lib/dbschema-kit/schema-printer
 */

const VALID_REL_FIELDS = ['type', 'target', 'localKey', 'references', 'onDelete', 'onUpdate'];

function serialize(ir, options) {
    if (!ir || typeof ir !== 'object' || typeof ir.tableName !== 'string') {
        throw new Error('serialize: ir must be an object with tableName');
    }
    const opts = options || {};
    const timestamp = opts.timestamp || new Date().toISOString();

    const sections = [];

    if (typeof ir.schemaName === 'string' && ir.schemaName !== '') {
        sections.push([`  schema: '${ir.schemaName}'`]);
    }

    sections.push(serializeFieldsSection(ir));

    if (Array.isArray(ir.primaryKey) && ir.primaryKey.length > 1) {
        sections.push(serializePrimaryKeySection(ir.primaryKey));
    }

    const compositeUniques = collectCompositeUniques(ir.uniques);
    if (compositeUniques.length > 0) {
        sections.push(serializeUniquesSection(compositeUniques));
    }

    const indexesToEmit = filterRedundantIndexes(ir.indexes, ir.fields, ir.primaryKey, ir.uniques);
    if (indexesToEmit.length > 0) {
        sections.push(serializeIndexesSection(indexesToEmit));
    }

    const checksToEmit = collectEmittableChecks(ir.checks);
    const unparsableChecks = collectUnparsableChecks(ir.checks);
    if (checksToEmit.length > 0) {
        sections.push(serializeChecksSection(checksToEmit, unparsableChecks));
    } else if (unparsableChecks.length > 0) {
        // Even if no parsable checks, surface unparsable ones via comment block.
        sections.push(serializeUnparsableOnlyChecksSection(unparsableChecks));
    }

    const explicitRelations = collectExplicitRelations(ir.relations);
    if (Object.keys(explicitRelations).length > 0) {
        sections.push(serializeRelationsSection(explicitRelations));
    }

    const header = [
        "'use strict';",
        '',
        '/**',
        ` * Schema definition for \`${ir.tableName}\``,
        ' *',
        ' * Generated by: npx restforge schema introspect',
        ` * Generated at: ${timestamp}`,
        ' */',
        `module.exports = ({ defineModel }) => defineModel('${ir.tableName}', {`
    ];

    const bodyLines = joinSectionsWithCommas(sections);
    const footer = ['});'];

    return header.concat(bodyLines).concat(footer).join('\n') + '\n';
}

function joinSectionsWithCommas(sections) {
    const out = [];
    sections.forEach((sec, i) => {
        if (i > 0) out.push('');
        sec.forEach((line, j) => {
            const isLastLineOfSection = j === sec.length - 1;
            const isLastSection = i === sections.length - 1;
            if (isLastLineOfSection && !isLastSection) {
                out.push(line + ',');
            } else {
                out.push(line);
            }
        });
    });
    return out;
}

// ─────────────────────────────────────────────────────────────
// Fields section
// ─────────────────────────────────────────────────────────────

function serializeFieldsSection(ir) {
    const lines = ['  fields: {'];
    const fieldNames = Object.keys(ir.fields || {});
    if (fieldNames.length === 0) {
        lines.push('  }');
        return lines;
    }

    const omitPkShorthand = Array.isArray(ir.primaryKey) && ir.primaryKey.length > 1;

    fieldNames.forEach((name, i) => {
        const field = ir.fields[name];

        if (field._defaultComment) {
            lines.push(`    // TODO: original DEFAULT '${field._defaultComment}' not mappable, define manually`);
        }

        const shorthand = fieldToShorthand(field, { omitPk: omitPkShorthand });
        const trailing = i < fieldNames.length - 1 ? ',' : '';
        let line = `    ${name}: ${quoteShorthand(shorthand)}${trailing}`;
        if (field._comment) {
            line += `  // TODO: ${field._comment}`;
        }
        lines.push(line);
    });

    lines.push('  }');
    return lines;
}

function fieldToShorthand(field, opts) {
    const tokens = [typeToken(field)];
    const isPk = field.pk === true && !(opts && opts.omitPk);
    if (isPk) {
        tokens.push('pk');
    } else if (field.notnull === true) {
        tokens.push('notnull');
    }
    if (field.unique === true) tokens.push('unique');
    if (field.default !== undefined) tokens.push(defaultToken(field.default));
    if (field.fk && !opts?.omitFk) tokens.push(`fk:${field.fk.table}.${field.fk.column}`);
    if (field.autoUpdate === true) tokens.push('autoUpdate');
    return tokens.join(' ');
}

function typeToken(field) {
    if (field.type === 'string') {
        const len = field.length != null ? field.length : '';
        return `string:${len}`;
    }
    if (field.type === 'decimal') {
        return `decimal:${field.precision},${field.scale}`;
    }
    return field.type;
}

function defaultToken(def) {
    if (!def || typeof def !== 'object') return `default:${String(def)}`;
    if (def.kind === 'literal') {
        const v = def.value;
        if (typeof v === 'string') return `default:'${v}'`;
        return `default:${v}`;
    }
    if (def.kind === 'function') {
        return `default:${def.name}()`;
    }
    if (def.kind === 'constant') {
        return `default:${def.value}`;
    }
    return `default:${String(def)}`;
}

function quoteShorthand(s) {
    // Wrap shorthand in single quote unless it contains a single quote literal
    // (e.g., default:'active') — then use double quote outer.
    if (s.indexOf("'") === -1) return `'${s}'`;
    if (s.indexOf('"') === -1) return `"${s}"`;
    // Both quote chars present — escape backticks and use template literal.
    return '`' + s.replace(/`/g, '\\`') + '`';
}

// ─────────────────────────────────────────────────────────────
// primaryKey section (composite only)
// ─────────────────────────────────────────────────────────────

function serializePrimaryKeySection(primaryKey) {
    const cols = primaryKey.map((c) => `'${c}'`).join(', ');
    return [`  primaryKey: [${cols}]`];
}

// ─────────────────────────────────────────────────────────────
// uniques section
// ─────────────────────────────────────────────────────────────

function collectCompositeUniques(uniques) {
    if (!Array.isArray(uniques)) return [];
    return uniques.filter((u) => Array.isArray(u.columns) && u.columns.length > 1);
}

function serializeUniquesSection(uniques) {
    const lines = ['  uniques: ['];
    uniques.forEach((u, i) => {
        const trailing = i < uniques.length - 1 ? ',' : '';
        const inline = `[${u.columns.map((c) => `'${c}'`).join(', ')}]`;
        lines.push(`    ${inline}${trailing}`);
    });
    lines.push('  ]');
    return lines;
}

// ─────────────────────────────────────────────────────────────
// indexes section
// ─────────────────────────────────────────────────────────────

function filterRedundantIndexes(indexes, fields, primaryKey, uniques) {
    if (!Array.isArray(indexes)) return [];
    const pkKey = (primaryKey || []).slice().sort().join('|');
    const uniqueSets = (uniques || [])
        .filter((u) => Array.isArray(u.columns))
        .map((u) => u.columns.slice().sort().join('|'));

    const fieldsObj = fields || {};

    // Single-column unique declared via field shorthand also creates an implicit
    // unique-backed index. Track these via field.unique = true.
    const singleColUniqueShorthand = new Set();
    for (const [fname, f] of Object.entries(fieldsObj)) {
        if (f && f.unique === true) singleColUniqueShorthand.add(fname);
    }

    const out = [];
    for (const idx of indexes) {
        if (!idx || !Array.isArray(idx.columns) || idx.columns.length === 0) continue;
        const key = idx.columns.slice().sort().join('|');

        // Skip PK-backed indexes
        if (key === pkKey && pkKey !== '') continue;
        // Skip composite unique-backed indexes
        if (uniqueSets.includes(key)) continue;
        // Skip single-col unique-backed indexes (declared via shorthand)
        if (idx.columns.length === 1 && singleColUniqueShorthand.has(idx.columns[0])) continue;
        // Skip single-col FK indexes (auto-derived by ir-builder on round-trip)
        if (idx.columns.length === 1) {
            const f = fieldsObj[idx.columns[0]];
            if (f && f.fk) continue;
        }

        out.push(idx);
    }
    return out;
}

function serializeIndexesSection(indexes) {
    const lines = ['  indexes: ['];
    indexes.forEach((idx, i) => {
        const trailing = i < indexes.length - 1 ? ',' : '';
        if (idx.columns.length === 1) {
            lines.push(`    '${idx.columns[0]}'${trailing}`);
        } else {
            const inline = `[${idx.columns.map((c) => `'${c}'`).join(', ')}]`;
            lines.push(`    ${inline}${trailing}`);
        }
    });
    lines.push('  ]');
    return lines;
}

// ─────────────────────────────────────────────────────────────
// checks section
// ─────────────────────────────────────────────────────────────

function collectEmittableChecks(checks) {
    if (!Array.isArray(checks)) return [];
    return checks.filter((c) => c && !c._unparsable && !c._invalid && c.field && c.op);
}

function collectUnparsableChecks(checks) {
    if (!Array.isArray(checks)) return [];
    return checks.filter((c) => c && c._unparsable);
}

function serializeChecksSection(checks, unparsable) {
    const lines = ['  checks: ['];
    checks.forEach((c, i) => {
        const trailing = i < checks.length - 1 || (unparsable && unparsable.length > 0) ? ',' : '';
        lines.push(`    ${formatCheckEntry(c)}${trailing}`);
    });
    if (unparsable && unparsable.length > 0) {
        for (const u of unparsable) {
            lines.push(`    // TODO: complex CHECK constraint not parsable: ${u.expression}`);
        }
    }
    lines.push('  ]');
    return lines;
}

function serializeUnparsableOnlyChecksSection(unparsable) {
    const lines = ['  // TODO: CHECK constraints below are not parsable, declare manually:'];
    for (const u of unparsable) {
        lines.push(`  //   - ${u.expression}`);
    }
    return lines;
}

function formatCheckEntry(c) {
    let valueRepr;
    if (c.op === 'in' && Array.isArray(c.value)) {
        valueRepr = `[${c.value.map(formatLiteralValue).join(', ')}]`;
    } else {
        valueRepr = formatLiteralValue(c.value);
    }
    let body = `field: '${c.field}', ${c.op}: ${valueRepr}`;
    if (c.name) body += `, name: '${c.name}'`;
    return `{ ${body} }`;
}

function formatLiteralValue(v) {
    if (typeof v === 'string') return `'${v.replace(/'/g, "\\'")}'`;
    if (typeof v === 'boolean') return String(v);
    if (typeof v === 'number') return String(v);
    if (v === null) return 'null';
    return String(v);
}

// ─────────────────────────────────────────────────────────────
// relations section
// ─────────────────────────────────────────────────────────────

function collectExplicitRelations(relations) {
    if (!relations || typeof relations !== 'object') return {};
    const out = {};
    for (const [name, rel] of Object.entries(relations)) {
        if (rel && rel._autoGenerated === true) continue;
        out[name] = rel;
    }
    return out;
}

function serializeRelationsSection(relations) {
    const lines = ['  relations: {'];
    const entries = Object.entries(relations);
    entries.forEach(([name, rel], i) => {
        const trailing = i < entries.length - 1 ? ',' : '';
        lines.push(`    ${name}: {`);

        const props = [];
        for (const key of VALID_REL_FIELDS) {
            const value = rel[key];
            if (value === undefined || value === null) continue;
            props.push(`${key}: '${value}'`);
        }
        props.forEach((p, j) => {
            const propTrailing = j < props.length - 1 ? ',' : '';
            lines.push(`      ${p}${propTrailing}`);
        });

        lines.push(`    }${trailing}`);
    });
    lines.push('  }');
    return lines;
}

module.exports = { serialize };
