"use strict";

const SECTION_STATUSES = Object.freeze(["editing", "submitted", "approved", "rejected"]);
const SECTION_COMMIT_KINDS = Object.freeze(["manual", "restore"]);
const SECTION_SUBMISSION_STATUSES = Object.freeze(["pending", "approved", "rejected", "conflicted"]);
const SNAPSHOT_OPERATIONS = Object.freeze(["create", "update", "delete", "reference", "replace"]);

/**
 * @typedef {"editing"|"submitted"|"approved"|"rejected"} SectionStatus
 * @typedef {"manual"|"restore"} SectionCommitKind
 * @typedef {"pending"|"approved"|"rejected"|"conflicted"} SectionSubmissionStatus
 * @typedef {"create"|"update"|"delete"|"reference"|"replace"} SnapshotOperation
 */

function normalizeNullableString(value) {
    if (value === undefined || value === null) return null;
    const normalized = String(value);
    return normalized.length ? normalized : null;
}

function normalizeRequiredString(value, fallback = "") {
    if (value === undefined || value === null) return fallback;
    return String(value);
}

function normalizeInteger(value, fallback = 0) {
    const parsed = Number(value);
    return Number.isFinite(parsed) ? Math.trunc(parsed) : fallback;
}

function normalizeStatus(value, allowed, fallback) {
    const normalized = String(value || "").trim().toLowerCase();
    return allowed.includes(normalized) ? normalized : fallback;
}

function normalizeIdArray(value) {
    if (!Array.isArray(value)) return [];
    const seen = new Set();
    const ids = [];
    for (const item of value) {
        if (typeof item !== "string" && typeof item !== "number") continue;
        const id = String(item).trim();
        if (!id || seen.has(id)) continue;
        seen.add(id);
        ids.push(id);
    }
    return ids;
}

function normalizeEntityContract(row) {
    return {
        id: normalizeRequiredString(row.id),
        name: normalizeRequiredString(row.name),
        slug: normalizeNullableString(row.slug),
        description: normalizeNullableString(row.description),
        type_id: normalizeNullableString(row.type_id),
        status: row.status === undefined || row.status === null ? null : normalizeInteger(row.status, 1),
        created_at: normalizeNullableString(row.created_at),
        updated_at: normalizeNullableString(row.updated_at),
        geometry_count: normalizeInteger(row.geometry_count, 0),
    };
}

function normalizeFeaturePropertiesContract(properties) {
    return {
        id: properties.id,
        type: normalizeNullableString(properties.type),
        time_start: properties.time_start === undefined || properties.time_start === null
            ? null
            : normalizeInteger(properties.time_start, 0),
        time_end: properties.time_end === undefined || properties.time_end === null
            ? null
            : normalizeInteger(properties.time_end, 0),
        binding: normalizeIdArray(properties.binding),
        entity_id: normalizeNullableString(properties.entity_id),
        entity_ids: normalizeIdArray(properties.entity_ids),
        entity_name: normalizeNullableString(properties.entity_name),
        entity_names: Array.isArray(properties.entity_names)
            ? properties.entity_names
                .filter((name) => typeof name === "string" && name.length > 0)
            : [],
        entity_type_id: normalizeNullableString(properties.entity_type_id),
    };
}

function normalizeFeatureContract(feature) {
    return {
        type: "Feature",
        properties: normalizeFeaturePropertiesContract(feature.properties || {}),
        geometry: feature.geometry,
    };
}

function normalizeFeatureCollectionContract(collection) {
    return {
        type: "FeatureCollection",
        features: Array.isArray(collection?.features)
            ? collection.features.map(normalizeFeatureContract)
            : [],
    };
}

function normalizeSectionStateContract(row) {
    return {
        section_id: normalizeRequiredString(row.section_id),
        status: normalizeStatus(row.status, SECTION_STATUSES, "editing"),
        head_commit_id: normalizeNullableString(row.head_commit_id),
        version: normalizeInteger(row.version, 0),
        locked_by: normalizeNullableString(row.locked_by),
        locked_at: normalizeNullableString(row.locked_at),
        lock_expires_at: normalizeNullableString(row.lock_expires_at),
        updated_at: normalizeRequiredString(row.updated_at),
    };
}

function normalizeSectionContract(row) {
    return {
        id: normalizeRequiredString(row.id),
        title: normalizeRequiredString(row.title),
        description: normalizeNullableString(row.description),
        user_id: normalizeNullableString(row.user_id),
        created_by: normalizeNullableString(row.created_by),
        created_at: normalizeRequiredString(row.created_at),
        updated_at: normalizeRequiredString(row.updated_at),
        state: {
            status: normalizeStatus(row.status, SECTION_STATUSES, "editing"),
            head_commit_id: normalizeNullableString(row.head_commit_id),
            version: normalizeInteger(row.version, 0),
            locked_by: normalizeNullableString(row.locked_by),
            locked_at: normalizeNullableString(row.locked_at),
            lock_expires_at: normalizeNullableString(row.lock_expires_at),
        },
    };
}

function normalizeSectionCommitContract(row, snapshot) {
    const out = {
        id: normalizeRequiredString(row.id),
        section_id: normalizeRequiredString(row.section_id),
        parent_commit_id: normalizeNullableString(row.parent_commit_id),
        commit_no: normalizeInteger(row.commit_no, 0),
        kind: normalizeStatus(row.kind, SECTION_COMMIT_KINDS, "manual"),
        restored_from_commit_id: normalizeNullableString(row.restored_from_commit_id),
        created_by: normalizeRequiredString(row.created_by),
        created_at: normalizeRequiredString(row.created_at),
        title: normalizeNullableString(row.title),
        note: normalizeNullableString(row.note),
        snapshot_hash: normalizeNullableString(row.snapshot_hash),
    };
    if (arguments.length >= 2) out.snapshot = normalizeEditorSnapshotContract(snapshot);
    return out;
}

function normalizeSectionSubmissionContract(row, snapshot) {
    const out = {
        id: normalizeRequiredString(row.id),
        section_id: normalizeRequiredString(row.section_id),
        commit_id: normalizeRequiredString(row.commit_id),
        submitted_by: normalizeRequiredString(row.submitted_by),
        submitted_at: normalizeRequiredString(row.submitted_at),
        status: normalizeStatus(row.status, SECTION_SUBMISSION_STATUSES, "pending"),
        reviewed_by: normalizeNullableString(row.reviewed_by),
        reviewed_at: normalizeNullableString(row.reviewed_at),
        review_note: normalizeNullableString(row.review_note),
        snapshot_hash: normalizeNullableString(row.snapshot_hash),
    };
    if (arguments.length >= 2) out.snapshot = normalizeEditorSnapshotContract(snapshot);
    return out;
}

function normalizeSnapshotOperation(value) {
    return normalizeStatus(value, SNAPSHOT_OPERATIONS, "");
}

function normalizeEditorSnapshotContract(snapshot) {
    if (!snapshot || typeof snapshot !== "object" || Array.isArray(snapshot)) return null;
    return {
        schema_version: normalizeInteger(snapshot.schema_version, 1),
        section: {
            id: normalizeRequiredString(snapshot.section?.id),
            title: normalizeRequiredString(snapshot.section?.title),
        },
        editor_feature_collection: snapshot.editor_feature_collection
            ? normalizeFeatureCollectionContract(snapshot.editor_feature_collection)
            : undefined,
        entities: Array.isArray(snapshot.entities) ? snapshot.entities : [],
        geometries: Array.isArray(snapshot.geometries) ? snapshot.geometries : [],
        link_scopes: Array.isArray(snapshot.link_scopes) ? snapshot.link_scopes : [],
    };
}

function assertEditorSnapshotContract(snapshot) {
    if (!snapshot || typeof snapshot !== "object" || Array.isArray(snapshot)) {
        throwContractError("snapshot must be an object");
    }
    if (!snapshot.section || typeof snapshot.section !== "object" || Array.isArray(snapshot.section)) {
        throwContractError("snapshot.section must be an object");
    }
    if (!normalizeRequiredString(snapshot.section.id)) {
        throwContractError("snapshot.section.id is required");
    }
    if (!normalizeRequiredString(snapshot.section.title)) {
        throwContractError("snapshot.section.title is required");
    }

    for (const entity of Array.isArray(snapshot.entities) ? snapshot.entities : []) {
        const operation = normalizeSnapshotOperation(entity?.operation);
        if (!operation) throwContractError("Invalid entity operation");
        if (!normalizeRequiredString(entity?.id)) throwContractError("Entity id is required");
    }

    for (const geometry of Array.isArray(snapshot.geometries) ? snapshot.geometries : []) {
        const operation = normalizeSnapshotOperation(geometry?.operation);
        if (!operation) throwContractError("Invalid geometry operation");
        if (!normalizeRequiredString(geometry?.id)) throwContractError("Geometry id is required");
    }

    for (const scope of Array.isArray(snapshot.link_scopes) ? snapshot.link_scopes : []) {
        if (!normalizeRequiredString(scope?.geometry_id)) throwContractError("link_scope geometry_id is required");
        if (!Array.isArray(scope?.entity_ids)) throwContractError("link_scope entity_ids must be an array");
    }
}

function throwContractError(message) {
    const err = new Error(message);
    err.status = 400;
    throw err;
}

module.exports = {
    SECTION_STATUSES,
    SECTION_COMMIT_KINDS,
    SECTION_SUBMISSION_STATUSES,
    SNAPSHOT_OPERATIONS,
    assertEditorSnapshotContract,
    normalizeEditorSnapshotContract,
    normalizeEntityContract,
    normalizeFeatureCollectionContract,
    normalizeFeatureContract,
    normalizeIdArray,
    normalizeSectionCommitContract,
    normalizeSectionContract,
    normalizeSectionStateContract,
    normalizeSectionSubmissionContract,
    normalizeSnapshotOperation,
};
