const SCHEMA_VERSION = 1;
const MIN_SCORE = -100;
const MAX_SCORE = 100;

export class ConnectionOpinionModel {
    static SCHEMA_VERSION = SCHEMA_VERSION;
    static MIN_SCORE = MIN_SCORE;
    static MAX_SCORE = MAX_SCORE;

    static create() {
        return { schemaVersion: SCHEMA_VERSION, byObserver: {} };
    }

    static isTracked(edge = {}) {
        return !!edge?.opinions && typeof edge.opinions === "object";
    }

    static canTrack(sourceNode = {}, targetNode = {}) {
        return this.#isSocialNode(sourceNode) && this.#isSocialNode(targetNode);
    }

    static shouldTrackByDefault(sourceNode = {}, targetNode = {}) {
        return this.canTrack(sourceNode, targetNode);
    }

    static normalize(opinions, { sourceNodeId = "", targetNodeId = "" } = {}) {
        if (!opinions || typeof opinions !== "object" || Array.isArray(opinions)) return null;

        const allowedObservers = new Set([sourceNodeId, targetNodeId].filter(Boolean));
        const byObserver = {};
        for (const [observerNodeId, opinion] of Object.entries(opinions.byObserver || {})) {
            if (!allowedObservers.has(observerNodeId) || !opinion || typeof opinion !== "object") continue;
            const normalized = this.normalizePerspective(opinion);
            if (normalized) byObserver[observerNodeId] = normalized;
        }

        return { schemaVersion: SCHEMA_VERSION, byObserver };
    }

    static normalizePerspective(perspective = {}) {
        const base = this.normalizeValue(perspective.base);
        const adjustments = [];
        const seen = new Set();
        for (const adjustment of Array.isArray(perspective.adjustments) ? perspective.adjustments : []) {
            const normalized = this.normalizeAdjustment(adjustment);
            if (!normalized || seen.has(normalized.id)) continue;
            seen.add(normalized.id);
            adjustments.push(normalized);
        }
        if (!base && !adjustments.length) return null;
        return { base, adjustments };
    }

    static normalizeAdjustment(adjustment = {}) {
        const id = String(adjustment?.id || "").trim();
        const label = String(adjustment?.label || "").trim();
        const value = this.normalizeValue(adjustment?.value);
        if (!id || !label || !value) return null;

        const sourceKind = String(adjustment?.source?.kind || "manual").trim() || "manual";
        const sourceId = String(adjustment?.source?.id || "").trim();
        return {
            id,
            label,
            value,
            source: { kind: sourceKind, id: sourceId || null },
            createdTime: Number(adjustment?.createdTime || Date.now()),
            updatedTime: Number(adjustment?.updatedTime || adjustment?.createdTime || Date.now())
        };
    }

    static normalizeValue(value) {
        const number = Number(value || 0);
        if (!Number.isFinite(number)) return 0;
        return Math.max(MIN_SCORE, Math.min(MAX_SCORE, Math.round(number)));
    }

    static getPerspective(opinions = {}, observerNodeId = "") {
        const perspective = opinions?.byObserver?.[observerNodeId] || null;
        return perspective
            ? {
                base: this.normalizeValue(perspective.base),
                adjustments: (perspective.adjustments || []).map(adjustment => ({
                    ...adjustment,
                    source: { ...(adjustment.source || {}) }
                }))
            }
            : { base: 0, adjustments: [] };
    }

    static getSubjectNodeId(edge = {}, observerNodeId = "") {
        if (edge.sourceNodeId === observerNodeId) return edge.targetNodeId || "";
        if (edge.targetNodeId === observerNodeId) return edge.sourceNodeId || "";
        return "";
    }

    static clampScore(value) {
        return this.normalizeValue(value);
    }

    static getTier(score = 0) {
        const value = this.clampScore(score);
        if (value >= 75) return { id: "deeply-favorable", label: "Deeply Favorable", color: "#65d88b" };
        if (value >= 40) return { id: "favorable", label: "Favorable", color: "#88cf72" };
        if (value >= 10) return { id: "cordial", label: "Cordial", color: "#b5cb77" };
        if (value >= -9) return { id: "neutral", label: "Neutral", color: "#aab4c0" };
        if (value >= -39) return { id: "wary", label: "Wary", color: "#d3ad62" };
        if (value >= -74) return { id: "hostile", label: "Hostile", color: "#d36f5e" };
        return { id: "bitter", label: "Bitter", color: "#c84f65" };
    }

    static #isSocialNode(node = {}) {
        return node.kind === "nexus-entity" && ["npc", "faction"].includes(node.entityType);
    }
}
