const crypto = require("crypto");
const db = require("../db/polygons");

function applyEntityBatchChanges(changes, options = {}) {
    const now = options.now || new Date().toISOString();
    const createdEntityIds = [];

    for (const change of changes) {
        const hasAction = Object.prototype.hasOwnProperty.call(change || {}, "action");
        const action = normalizeBatchAction(hasAction ? change.action : change?.type);

        if (!change || !action) {
            throw createValidationError("Invalid entity change entry");
        }

        if (action === "create") {
            const payload = normalizeEntityCreatePayload(change);
            const id = payload.id || crypto.randomUUID();

            try {
                db.prepare(`
                    INSERT INTO entities (
                        id, name, slug, description, type_id, status, is_deleted, created_at, updated_at
                    )
                    VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?)
                `).run(
                    id,
                    payload.name,
                    payload.slug,
                    payload.description,
                    payload.typeId,
                    payload.status,
                    now,
                    now
                );
            } catch (err) {
                if (isSqliteConstraint(err)) {
                    throw createValidationError("Entity name/slug must be unique", 409);
                }
                throw err;
            }

            createdEntityIds.push(id);
            continue;
        }

        if (action === "update") {
            const payload = normalizeEntityUpdatePayload(change);
            const existing = db.prepare(`
                SELECT *
                FROM entities
                WHERE id = ?
                  AND is_deleted = 0
            `).get(payload.id);

            if (!existing) {
                continue;
            }

            const hasName = Object.prototype.hasOwnProperty.call(payload.fields, "name");
            const hasSlug = Object.prototype.hasOwnProperty.call(payload.fields, "slug");
            const hasDescription = Object.prototype.hasOwnProperty.call(payload.fields, "description");
            const hasTypeId = Object.prototype.hasOwnProperty.call(payload.fields, "type_id");
            const hasStatus = Object.prototype.hasOwnProperty.call(payload.fields, "status");

            if (!hasName && !hasSlug && !hasDescription && !hasTypeId && !hasStatus) {
                throw createValidationError("Invalid entity update payload");
            }

            const name = hasName ? normalizeRequiredString(payload.fields.name) : existing.name;
            if (!name) {
                throw createValidationError("name cannot be empty");
            }

            const slug = hasSlug ? normalizeOptionalString(payload.fields.slug) : existing.slug;
            const description = hasDescription
                ? normalizeOptionalString(payload.fields.description)
                : existing.description;
            const typeId = hasTypeId ? normalizeTypeId(payload.fields.type_id) : existing.type_id;
            const status = hasStatus
                ? (normalizeOptionalNumber(payload.fields.status) ?? existing.status)
                : existing.status;

            try {
                db.prepare(`
                    UPDATE entities
                    SET name = ?,
                        slug = ?,
                        description = ?,
                        type_id = ?,
                        status = ?,
                        updated_at = ?
                    WHERE id = ?
                      AND is_deleted = 0
                `).run(
                    name,
                    slug,
                    description,
                    typeId,
                    status,
                    now,
                    payload.id
                );
            } catch (err) {
                if (isSqliteConstraint(err)) {
                    throw createValidationError("Entity name/slug must be unique", 409);
                }
                throw err;
            }
            continue;
        }

        if (action === "delete") {
            const id = normalizeDeleteEntityId(change);
            if (!id) {
                throw createValidationError("Invalid entity delete payload");
            }

            const existing = db.prepare(`
                SELECT id
                FROM entities
                WHERE id = ?
                  AND is_deleted = 0
            `).get(id);

            if (!existing) {
                continue;
            }

            assertEntityCanBeDeleted(id);

            db.prepare(`
                UPDATE entities
                SET is_deleted = 1,
                    updated_at = ?
                WHERE id = ?
                  AND is_deleted = 0
            `).run(now, id);

            db.prepare(`
                DELETE FROM entity_geometries
                WHERE entity_id = ?
            `).run(id);
            continue;
        }

        throw createValidationError(`Unknown entity change type: ${String(action)}`);
    }

    return {
        applied: changes.length,
        createdEntityIds,
    };
}

module.exports = {
    applyEntityBatchChanges,
};

function normalizeEntityCreatePayload(change) {
    const source = isPlainObject(change.entity) ? change.entity : change;
    const name = normalizeRequiredString(source?.name);
    if (!name) {
        throw createValidationError("Invalid entity create payload: name is required");
    }

    const id = normalizeEntityId(source?.id);
    const slug = Object.prototype.hasOwnProperty.call(source || {}, "slug")
        ? normalizeOptionalString(source.slug)
        : toSlug(name);
    const description = normalizeOptionalString(source?.description);
    const typeId = normalizeTypeId(source?.type_id);
    const status = normalizeOptionalNumber(source?.status) ?? 1;

    return {
        id,
        name,
        slug,
        description,
        typeId,
        status,
    };
}

function normalizeEntityUpdatePayload(change) {
    const source = isPlainObject(change.entity) ? change.entity : change;
    const id = normalizeEntityId(source?.id ?? change?.id);
    if (!id) {
        throw createValidationError("Invalid entity update payload: id is required");
    }

    return {
        id,
        fields: source,
    };
}

function normalizeDeleteEntityId(change) {
    const source = isPlainObject(change.entity) ? change.entity : null;
    return normalizeEntityId(change?.id ?? source?.id);
}

function assertEntityCanBeDeleted(entityId) {
    const orphanedGeometryRows = db.prepare(`
        SELECT eg_target.geometry_id
        FROM entity_geometries eg_target
        JOIN geometries g_target
            ON g_target.id = eg_target.geometry_id
           AND g_target.is_deleted = 0
        LEFT JOIN entity_geometries eg_other
            ON eg_other.geometry_id = eg_target.geometry_id
           AND eg_other.entity_id <> eg_target.entity_id
        LEFT JOIN entities e_other
            ON e_other.id = eg_other.entity_id
           AND e_other.is_deleted = 0
        WHERE eg_target.entity_id = ?
        GROUP BY eg_target.geometry_id
        HAVING COUNT(e_other.id) = 0
    `).all(entityId);

    if (!orphanedGeometryRows.length) return;

    const previewIds = orphanedGeometryRows.slice(0, 10).map((row) => row.geometry_id);
    const suffix = orphanedGeometryRows.length > 10 ? ", ..." : "";
    throw createValidationError(
        `Cannot delete entity. Reassign ${orphanedGeometryRows.length} linked geometries first: ${previewIds.join(", ")}${suffix}`,
        409
    );
}

function normalizeBatchAction(value) {
    if (value === undefined || value === null) return null;
    const normalized = String(value).trim().toLowerCase();
    if (normalized === "create" || normalized === "update" || normalized === "delete") {
        return normalized;
    }
    return null;
}

function normalizeEntityId(value) {
    if (value === undefined || value === null || value === "") return null;
    const normalized = String(value).trim();
    return normalized || null;
}

function normalizeRequiredString(value) {
    if (typeof value !== "string") return null;
    const trimmed = value.trim();
    return trimmed.length ? trimmed : null;
}

function normalizeOptionalString(value) {
    if (value === undefined || value === null) return null;
    if (typeof value !== "string") return null;
    const trimmed = value.trim();
    return trimmed.length ? trimmed : null;
}

function normalizeOptionalNumber(value) {
    if (value === undefined || value === null || value === "") return null;
    const num = Number(value);
    if (!Number.isFinite(num)) return null;
    return num;
}

function normalizeTypeId(value) {
    if (value === undefined || value === null || value === "") {
        return "country";
    }
    const trimmed = String(value).trim().toLowerCase();
    return trimmed || "country";
}

function toSlug(value) {
    return value
        .normalize("NFKD")
        .replace(/[\u0300-\u036f]/g, "")
        .toLowerCase()
        .replace(/[^a-z0-9]+/g, "-")
        .replace(/^-+|-+$/g, "")
        .slice(0, 120) || null;
}

function isPlainObject(value) {
    return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}

function isSqliteConstraint(error) {
    if (!error || typeof error !== "object") return false;
    const code = error.code || "";
    if (code === "SQLITE_CONSTRAINT" || String(code).startsWith("SQLITE_CONSTRAINT_")) {
        return true;
    }
    const message = typeof error.message === "string" ? error.message : "";
    return message.includes("UNIQUE constraint failed");
}

function createValidationError(message, status = 400) {
    const err = new Error(message);
    err.status = status;
    return err;
}
