// Suggestions the furni editor derives from data it already holds: the
// furnidata entry, the classname and the interaction type. Each one names a
// field, the value to put there and the reason, so the view can offer it as a
// one-click chip and the diff modal still confirms the save.

export interface EditableFields {
    width: number;
    length: number;
    stackHeight: number;
    allowStack: boolean;
    allowWalk: boolean;
    allowSit: boolean;
    allowLay: boolean;
    allowTrade: boolean;
    allowRecycle: boolean;
    interactionType: string;
    interactionModesCount: number;
    vendingIds: string;
    multiheight: string;
}

export type SuggestionField = keyof EditableFields;

export interface Suggestion<F extends SuggestionField = SuggestionField> {
    field: F;
    value: EditableFields[F];
    reason: string;
}

// A requirement the type has that the form does not meet and no value can be
// guessed for: shown as a warning, never applied.
export interface Expectation {
    field: SuggestionField;
    message: string;
}

// Types too generic to be inferred from a classname token: "default" would match
// half the hotel and "multiheight" is a behaviour, not a name.
const UNSUGGESTABLE_TYPES = new Set(['default', 'multiheight']);

export const suggestInteractionType = (classname: string, registered: string[]): { type: string; reason: string } | null => {
    const name = classname.trim().toLowerCase();
    if (!name) return null;
    const candidates = registered.filter((type) => !UNSUGGESTABLE_TYPES.has(type.toLowerCase()));

    const exact = candidates.find((type) => type.toLowerCase() === name);
    if (exact) return { type: exact, reason: 'classname is a registered type' };

    const prefix = candidates
        .filter((type) => name.startsWith(`${type.toLowerCase()}_`) || name.startsWith(`${type.toLowerCase()}-`))
        .sort((a, b) => b.length - a.length)[0];
    if (prefix) return { type: prefix, reason: 'classname starts with it' };

    const tokens = name.split(/[_\-*]/).filter(Boolean);
    const token = candidates.filter((type) => tokens.includes(type.toLowerCase())).sort((a, b) => b.length - a.length)[0];
    if (token) return { type: token, reason: 'classname contains it' };

    return null;
};

const asInt = (value: unknown): number | null => {
    const n = typeof value === 'number' ? value : typeof value === 'string' ? Number(value) : NaN;
    return Number.isFinite(n) ? Math.trunc(n) : null;
};

const asBool = (value: unknown): boolean | null => (typeof value === 'boolean' ? value : value === 'true' ? true : value === 'false' ? false : null);

// What the furnidata entry says about the furni versus what items_base holds.
// The entry is trusted only when the caller has matched it by classname.
export const suggestFromFurnidata = (entry: Record<string, unknown> | null, form: EditableFields): Suggestion[] => {
    if (!entry) return [];
    const out: Suggestion[] = [];
    const from = 'furnidata';

    const xdim = asInt(entry.xdim);
    const ydim = asInt(entry.ydim);
    if (xdim !== null && xdim >= 1 && xdim !== form.width) out.push({ field: 'width', value: xdim, reason: `${from} xdim` });
    if (ydim !== null && ydim >= 1 && ydim !== form.length) out.push({ field: 'length', value: ydim, reason: `${from} ydim` });

    const height = typeof entry.height === 'number' ? entry.height : typeof entry.height === 'string' ? Number(entry.height) : NaN;
    if (Number.isFinite(height) && height >= 0 && Math.abs(height - form.stackHeight) > 0.001)
        out.push({ field: 'stackHeight', value: height, reason: `${from} height` });

    const flags: [SuggestionField & ('allowWalk' | 'allowSit' | 'allowLay' | 'allowTrade' | 'allowRecycle'), string][] = [
        ['allowWalk', 'canstandon'],
        ['allowSit', 'cansiton'],
        ['allowLay', 'canlayon'],
        ['allowTrade', 'tradeable'],
        ['allowRecycle', 'recyclable']
    ];
    for (const [field, key] of flags) {
        const value = asBool(entry[key]);
        if (value !== null && value !== form[field]) out.push({ field, value, reason: `${from} ${key}` });
    }

    return out;
};

// What a registered interaction type needs from the other fields. Modes are
// the state count the interaction class drives; a type that reads a list
// (vending ids, multiheight) cannot work with that list empty.
const TYPE_MODES: Record<string, number> = {
    gate: 2,
    guild_gate: 2,
    teleport: 2,
    teleporttile: 2,
    pressureplate: 2,
    one_way_gate: 2,
    colorplate: 2
};

const TYPE_NEEDS: Record<string, { field: 'vendingIds' | 'multiheight'; message: string }> = {
    vendingmachine: { field: 'vendingIds', message: 'vendingmachine hands out nothing without vending ids' },
    multiheight: { field: 'multiheight', message: 'multiheight needs its list of heights' }
};

export const expectationsForType = (form: EditableFields): { suggestions: Suggestion[]; warnings: Expectation[] } => {
    const type = form.interactionType.trim().toLowerCase();
    const suggestions: Suggestion[] = [];
    const warnings: Expectation[] = [];
    if (!type) return { suggestions, warnings };

    const modes = TYPE_MODES[type];
    if (modes !== undefined && form.interactionModesCount !== modes) {
        suggestions.push({ field: 'interactionModesCount', value: modes, reason: `${type} drives ${modes} states` });
    }

    const need = TYPE_NEEDS[type];
    if (need && !form[need.field].trim()) warnings.push({ field: need.field, message: need.message });

    return { suggestions, warnings };
};

// The renderer resolves a type id to its classname through the furnidata id,
// so an entry matched by classname whose id is not the sprite id means the
// room draws a different furni than the one this row describes.
export const spriteIdMismatch = (entry: Record<string, unknown> | null, spriteId: number): number | null => {
    if (!entry) return null;
    const id = asInt(entry.id);
    return id !== null && id !== spriteId ? id : null;
};

// A multiheight furni needs one height per visualization state; the asset
// count, when known, says how many states that is.
export const multiheightMismatch = (form: EditableFields, assetStates: number | null): Expectation | null => {
    if (form.interactionType.trim().toLowerCase() !== 'multiheight' || assetStates === null || assetStates <= 0) return null;
    const heights = form.multiheight
        .split(',')
        .map((h) => h.trim())
        .filter(Boolean).length;
    if (heights === 0 || heights === assetStates) return null;
    return { field: 'multiheight', message: `${heights} height${heights === 1 ? '' : 's'} for ${assetStates} states in the asset` };
};

// A furni line: rows whose classname shares the prefix up to the last
// underscore (throne_gold, throne_silver → throne). The colour suffix *N is
// never part of it.
export const lineQueryFor = (classname: string): string => {
    const base = classname.split('*')[0].trim().toLowerCase();
    const cut = base.lastIndexOf('_');
    const prefix = cut >= 3 ? base.slice(0, cut) : base;
    return prefix;
};

export interface RelatedRow {
    id: number;
    spriteId: number;
    itemName: string;
    width: number;
    length: number;
    stackHeight: number;
    allowStack: boolean;
    allowWalk: boolean;
    allowSit: boolean;
    allowLay: boolean;
    interactionType: string;
    interactionModesCount: number;
}

export interface Related {
    siblings: RelatedRow[];
    duplicateNames: RelatedRow[];
    duplicateSprites: RelatedRow[];
}

export const relateRows = (rows: RelatedRow[], self: { id: number; itemName: string; spriteId: number }): Related => {
    const prefix = lineQueryFor(self.itemName);
    const ownName = self.itemName.split('*')[0].trim().toLowerCase();
    const others = rows.filter((row) => row.id !== self.id);
    return {
        siblings: others.filter((row) => {
            const name = row.itemName.split('*')[0].trim().toLowerCase();
            return name !== ownName && (name === prefix || name.startsWith(`${prefix}_`));
        }),
        duplicateNames: others.filter((row) => row.itemName.trim().toLowerCase() === self.itemName.trim().toLowerCase()),
        duplicateSprites: others.filter((row) => row.spriteId === self.spriteId)
    };
};

type LineField = 'width' | 'length' | 'stackHeight' | 'allowStack' | 'allowWalk' | 'allowSit' | 'allowLay' | 'interactionType' | 'interactionModesCount';
const LINE_FIELDS: LineField[] = [
    'width',
    'length',
    'stackHeight',
    'allowStack',
    'allowWalk',
    'allowSit',
    'allowLay',
    'interactionType',
    'interactionModesCount'
];

// The value most of the siblings share, when more than half agree and the
// form differs. One sibling is a coincidence, not a line convention.
export const suggestFromSiblings = (siblings: RelatedRow[], form: EditableFields & { allowStack: boolean }): Suggestion[] => {
    if (siblings.length < 2) return [];
    const out: Suggestion[] = [];
    for (const field of LINE_FIELDS) {
        const tally = new Map<string, { value: RelatedRow[LineField]; count: number }>();
        for (const row of siblings) {
            const value = row[field];
            const key = String(value);
            const entry = tally.get(key) ?? { value, count: 0 };
            entry.count += 1;
            tally.set(key, entry);
        }
        const best = [...tally.values()].sort((a, b) => b.count - a.count)[0];
        if (!best || best.count * 2 <= siblings.length) continue;
        if (String(form[field]) === String(best.value)) continue;
        out.push({ field, value: best.value as never, reason: `${best.count} of ${siblings.length} in the line` });
    }
    return out;
};
