import type { ProjectCommit } from "@/uhm/api/projects";
import type { EntitySnapshot } from "@/uhm/types/entities";
import type { Feature, Geometry, FeatureCollection } from "@/uhm/types/geo";
import type { BattleReplay } from "@/uhm/types/projects";
import type { WikiSnapshot } from "@/uhm/types/wiki";
import { normalizeTimelineYearValue } from "@/uhm/lib/utils/timeline";
import type { Entity } from "@/uhm/api/entities";
import { deepClone } from "@/uhm/lib/editor/draft/draftDiff";
import { normalizeFeatureEntityIds } from "@/uhm/lib/editor/snapshot/editorSnapshot";

// Giới hạn kích thước panel khi drag resize để tránh layout bị vỡ.
export function clampNumber(value: number, min: number, max: number): number {
    if (value < min) return min;
    if (value > max) return max;
    return value;
}

// Tạo label ngắn cho commit history, ưu tiên summary người dùng nhập.
export function formatCommitTitle(commit: ProjectCommit): string {
    return commit.edit_summary?.trim() || `Commit ${commit.id.slice(0, 8)}`;
}

// Kiểm tra feature có nằm trong năm timeline đang active hay không.
export function isFeatureVisibleAtYear(feature: Feature, year: number): boolean {
    const start = normalizeTimelineYearValue(feature.properties.time_start);
    const end = normalizeTimelineYearValue(feature.properties.time_end);
    if (start !== null && year < start) return false;
    if (end !== null && year > end) return false;
    return true;
}

function getFastHash(str: string | null | undefined): number {
    if (!str) return 0;
    let hash = 5381;
    for (let i = 0; i < str.length; i++) {
        hash = (hash * 33) ^ str.charCodeAt(i);
    }
    return hash >>> 0;
}

// Chuẩn hóa wiki snapshot để so sánh dirty-state ổn định, không phụ thuộc thứ tự mảng.
export function normalizeWikisForCompare(input: WikiSnapshot[] | null | undefined) {
    const list = Array.isArray(input) ? input : [];
    return list
        .filter((w) => w && typeof w.id === "string" && w.id.trim().length > 0)
        .filter((w) => {
            if (w.source === "ref") return true;
            if (w.operation === "create" || w.operation === "update" || w.operation === "delete") return true;
            const title = typeof w.title === "string" ? w.title.trim() : "";
            const doc = typeof w.doc === "string" ? w.doc.trim() : "";
            return title.length > 0 || (w.doc !== null && doc.length > 0);
        })
        .map((w) => ({
            id: w.id,
            source: w.source,
            title: typeof w.title === "string" ? w.title.trim() : "",
            slug: typeof w.slug === "string" ? w.slug : null,
            docHash: typeof w.doc === "string" ? getFastHash(w.doc.trim()) : 0,
        }))
        .sort((a, b) => a.id.localeCompare(b.id));
}

// Chuẩn hóa entity snapshot để phát hiện thay đổi name/description/source.
export function normalizeEntitiesForCompare(input: EntitySnapshot[] | null | undefined) {
    const list = Array.isArray(input) ? input : [];
    return list
        .filter((e) => e && (typeof e.id === "string" || typeof e.id === "number"))
        .map((e) => ({
            id: String(e.id),
            source: e.source,
            name: typeof e.name === "string" ? e.name.trim() : "",
            description: e.description == null ? null : String(e.description),
            time_start: normalizeTimelineYearValue(e.time_start),
            time_end: normalizeTimelineYearValue(e.time_end),
        }))
        .sort((a, b) => a.id.localeCompare(b.id));
}

// Chuẩn hóa binding entity-wiki để dirty check không bị nhiễu bởi thứ tự.
export function normalizeEntityWikiLinksForCompare(
    input: Array<{ entity_id: string; wiki_id: string; operation?: string }> | null | undefined
) {
    const list = Array.isArray(input) ? input : [];
    return list
        .filter((l) => l && typeof l.entity_id === "string" && typeof l.wiki_id === "string")
        .map((l) => ({
            entity_id: l.entity_id,
            wiki_id: l.wiki_id,
            operation: l.operation === "delete" ? "delete" : "binding",
        }))
        .sort((a, b) => (a.entity_id + a.wiki_id).localeCompare(b.entity_id + b.wiki_id));
}

// Chuẩn hóa replay để phát hiện thay đổi script/target geometry.
export function normalizeReplaysForCompare(input: BattleReplay[] | null | undefined) {
    const list = Array.isArray(input) ? input : [];
    return list
        .filter((replay) => replay && typeof replay.geometry_id === "string" && replay.geometry_id.trim().length > 0)
        .map((replay) => ({
            id: typeof replay.id === "string" ? replay.id : replay.geometry_id,
            geometry_id: replay.geometry_id,
            target_geometry_ids: normalizeReplayTargetGeometryIdsForCompare(
                replay.target_geometry_ids,
                replay.geometry_id
            ),
            detail: Array.isArray(replay.detail) ? replay.detail : [],
        }))
        .sort((a, b) => a.geometry_id.localeCompare(b.geometry_id));
}

// Bảo toàn geometry chính ở vị trí đầu và loại bỏ id trùng trong replay target list.
function normalizeReplayTargetGeometryIdsForCompare(
    input: string[] | null | undefined,
    geometryId: string
) {
    const orderedIds: string[] = [];
    const seen = new Set<string>();

    const pushId = (rawId: string | number | null | undefined) => {
        if (rawId == null) return;
        const id = String(rawId).trim();
        if (!id || seen.has(id)) return;
        seen.add(id);
        orderedIds.push(id);
    };

    pushId(geometryId);
    for (const rawId of input || []) pushId(rawId);
    return orderedIds;
}

// Validate tối thiểu geometry trả về từ search trước khi đưa vào draft.
export function normalizeGeoSearchGeometry(value: unknown): Geometry | null {
    if (!value || typeof value !== "object") return null;
    const geometry = value as Record<string, unknown>;
    if (typeof geometry.type !== "string") return null;
    if (!("coordinates" in geometry)) return null;
    return value as Geometry;
}

// Chuẩn hóa parent id từ API search GEO.
export function normalizeGeoSearchBoundWith(value: unknown): string | null {
    if (value == null) return null;
    if (typeof value !== "string" && typeof value !== "number") return null;
    const id = String(value).trim();
    return id.length ? id : null;
}

export function hasPlayableReplaySteps(replay: BattleReplay | null | undefined) {
    return Boolean(
        replay?.detail?.some((stage) => Array.isArray(stage?.steps) && stage.steps.length > 0)
    );
}

export function buildReplayPreviewDraftFromSource(sourceDraft: FeatureCollection, replay: BattleReplay): FeatureCollection {
    const targetIds = normalizeReplayPreviewTargetGeometryIds(replay);
    return {
        type: "FeatureCollection",
        features: targetIds
            .map((id) =>
                sourceDraft.features.find((feature) => String(feature.properties.id) === id) || null
            )
            .filter((feature): feature is Feature => Boolean(feature))
            .map((feature) => ({
                ...deepClone(feature),
                properties: {
                    ...deepClone(feature.properties),
                    bound_with: null,
                },
            })),
    };
}

export function normalizeReplayPreviewTargetGeometryIds(replay: BattleReplay) {
    const orderedIds: string[] = [];
    const seen = new Set<string>();
    const pushId = (rawId: string | number | null | undefined) => {
        if (rawId == null) return;
        const id = String(rawId).trim();
        if (!id.length || seen.has(id)) return;
        seen.add(id);
        orderedIds.push(id);
    };

    pushId(replay.geometry_id);
    for (const rawId of replay.target_geometry_ids || []) pushId(rawId);
    return orderedIds;
}

export function readImageAspectRatio(url: string): Promise<number> {
    return new Promise((resolve, reject) => {
        const image = new Image();
        image.onload = () => {
            const width = image.naturalWidth || image.width;
            const height = image.naturalHeight || image.height;
            if (!width || !height) {
                reject(new Error("Image has invalid dimensions."));
                return;
            }
            resolve(width / height);
        };
        image.onerror = () => reject(new Error("Image load failed."));
        image.src = url;
    });
}

export function isTypingTarget(target: EventTarget | null): boolean {
    if (!(target instanceof HTMLElement)) return false;
    const tagName = target.tagName.toLowerCase();
    return tagName === "input" || tagName === "textarea" || tagName === "select" || target.isContentEditable;
}

export function buildEntityRefsForFeature(feature: Feature, entities: Entity[]): Entity[] {
    const entityIds = normalizeFeatureEntityIds(feature);
    if (!entityIds.length) return [];

    const entityById = new globalThis.Map<string, Entity>();
    for (const entity of entities || []) {
        const id = String(entity?.id || "").trim();
        if (!id) continue;
        entityById.set(id, entity);
    }

    const entityNames = Array.isArray(feature.properties.entity_names)
        ? feature.properties.entity_names
        : [];
    const primaryName = typeof feature.properties.entity_name === "string"
        ? feature.properties.entity_name.trim()
        : "";

    return entityIds.map((id, index) => {
        const catalogEntity = entityById.get(id);
        if (catalogEntity) return catalogEntity;

        const name = String(entityNames[index] || (index === 0 ? primaryName : "") || id).trim() || id;
        return {
            id,
            name,
            description: null,
            time_start: null,
            time_end: null,
            geometry_count: 0,
        };
    });
}

export function toEntityRefSnapshot(entity: Entity): EntitySnapshot {
    return {
        id: String(entity.id),
        source: "ref",
        operation: "reference",
        name: entity.name,
        description: entity.description ?? null,
        time_start: normalizeTimelineYearValue(entity.time_start),
        time_end: normalizeTimelineYearValue(entity.time_end),
    };
}

export function mergeSnapshotEntityRefs(prev: EntitySnapshot[], refs: EntitySnapshot[]): EntitySnapshot[] {
    if (!refs.length) return prev;

    const refsById = new globalThis.Map<string, EntitySnapshot>();
    for (const ref of refs) {
        const id = String(ref?.id || "").trim();
        if (!id) continue;
        refsById.set(id, ref);
    }
    if (!refsById.size) return prev;

    let changed = false;
    const seen = new Set<string>();
    const next = (prev || []).map((row) => {
        const id = String(row?.id || "").trim();
        if (!id || !refsById.has(id)) return row;
        seen.add(id);
        if (row.operation !== "delete") return row;
        changed = true;
        return refsById.get(id) || row;
    });

    const missing = Array.from(refsById.values()).filter((ref) => !seen.has(String(ref.id)));
    if (missing.length) changed = true;
    return changed ? [...missing, ...next] : prev;
}

export function mergeEntityCatalogById(prev: Entity[], refs: Entity[]): Entity[] {
    if (!refs.length) return prev;

    const byId = new globalThis.Map<string, Entity>();
    for (const entity of prev || []) {
        const id = String(entity?.id || "").trim();
        if (!id) continue;
        byId.set(id, entity);
    }
    for (const entity of refs) {
        const id = String(entity?.id || "").trim();
        if (!id) continue;
        byId.set(id, entity);
    }
    return Array.from(byId.values());
}

export function parseOptionalEntityYearInput(value: string, fieldName: string): number | undefined {
    const trimmed = String(value || "").trim();
    if (!trimmed.length) return undefined;
    const parsed = Number(trimmed);
    if (!Number.isFinite(parsed) || !Number.isInteger(parsed)) {
        throw new Error(`${fieldName} phải là số nguyên.`);
    }
    return parsed;
}
