import DHAdversarySettings from '../../applications/sheets-configs/adversary-settings.mjs';
import { ActionField } from '../fields/actionField.mjs';
import { commonActorRules } from './base.mjs';
import DhCreature from './creature.mjs';
import { bonusField } from '../fields/actorField.mjs';
import { calculateExpectedValue, parseTermsFromSimpleFormula } from '../../helpers/utils.mjs';
import { adversaryExpectedDamage, adversaryScalingData } from '../../config/actorConfig.mjs';

export default class DhpAdversary extends DhCreature {
    static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Adversary'];

    static get metadata() {
        return foundry.utils.mergeObject(super.metadata, {
            label: 'TYPES.Actor.adversary',
            type: 'adversary',
            settingSheet: DHAdversarySettings,
            hasAttribution: true,
            usesSize: true
        });
    }

    static defineSchema() {
        const fields = foundry.data.fields;
        return {
            ...super.defineSchema(),
            tier: new fields.NumberField({
                required: true,
                integer: true,
                choices: CONFIG.DH.GENERAL.tiers,
                initial: CONFIG.DH.GENERAL.tiers[1].id
            }),
            type: new fields.StringField({
                required: true,
                choices: CONFIG.DH.ACTOR.allAdversaryTypes,
                initial: CONFIG.DH.ACTOR.adversaryTypes.standard.id
            }),
            motivesAndTactics: new fields.StringField(),
            notes: new fields.HTMLField(),
            difficulty: new fields.NumberField({ required: true, initial: 1, integer: true }),
            hordeHp: new fields.NumberField({
                required: true,
                initial: 1,
                integer: true,
                label: 'DAGGERHEART.GENERAL.hordeHp'
            }),
            criticalThreshold: new fields.NumberField({
                required: true,
                integer: true,
                min: 1,
                max: 20,
                initial: 20,
                label: 'DAGGERHEART.ACTIONS.Settings.criticalThreshold'
            }),
            damageThresholds: new fields.SchemaField({
                major: new fields.NumberField({
                    required: true,
                    initial: 0,
                    integer: true,
                    label: 'DAGGERHEART.GENERAL.DamageThresholds.majorThreshold'
                }),
                severe: new fields.NumberField({
                    required: true,
                    initial: 0,
                    integer: true,
                    label: 'DAGGERHEART.GENERAL.DamageThresholds.severeThreshold'
                })
            }),
            rules: new fields.SchemaField({
                ...commonActorRules()
            }),
            attack: new ActionField({
                initial: {
                    name: 'Attack',
                    img: 'icons/skills/melee/blood-slash-foam-red.webp',
                    _id: foundry.utils.randomID(),
                    systemPath: 'attack',
                    chatDisplay: false,
                    type: 'attack',
                    range: 'melee',
                    target: {
                        type: 'any',
                        amount: 1
                    },
                    roll: {
                        type: 'attack'
                    },
                    damage: {
                        parts: {
                            hitPoints: {
                                type: ['physical'],
                                value: {
                                    multiplier: 'flat'
                                }
                            }
                        }
                    }
                }
            }),
            experiences: new fields.TypedObjectField(
                new fields.SchemaField({
                    name: new fields.StringField(),
                    value: new fields.NumberField({ required: true, integer: true, initial: 1 }),
                    description: new fields.StringField()
                })
            ),
            bonuses: new fields.SchemaField({
                roll: new fields.SchemaField({
                    attack: bonusField('DAGGERHEART.GENERAL.Roll.attack'),
                    action: bonusField('DAGGERHEART.GENERAL.Roll.action'),
                    reaction: bonusField('DAGGERHEART.GENERAL.Roll.reaction')
                }),
                damage: new fields.SchemaField({
                    physical: bonusField('DAGGERHEART.GENERAL.Damage.physicalDamage'),
                    magical: bonusField('DAGGERHEART.GENERAL.Damage.magicalDamage')
                })
            })
        };
    }

    /* -------------------------------------------- */

    /**@inheritdoc */
    static DEFAULT_ICON = 'systems/daggerheart/assets/icons/documents/actors/dragon-head.svg';

    /* -------------------------------------------- */

    get attackBonus() {
        return this.attack.roll.bonus;
    }

    get features() {
        return this.parent.items.filter(x => x.type === 'feature');
    }

    isItemValid(source) {
        return super.isItemValid(source) || source.type === 'feature';
    }

    async _preUpdate(changes, options, user) {
        const allowed = await super._preUpdate(changes, options, user);
        if (allowed === false) return false;

        if (this.type === CONFIG.DH.ACTOR.adversaryTypes.horde.id) {
            const autoHordeDamage = game.settings.get(
                CONFIG.DH.id,
                CONFIG.DH.SETTINGS.gameSettings.Automation
            ).hordeDamage;
            if (autoHordeDamage && changes.system?.resources?.hitPoints?.value !== undefined) {
                const hordeActiveEffect = this.parent.effects.find(x => x.type === 'horde');
                if (hordeActiveEffect) {
                    const halfHP = Math.ceil(this.resources.hitPoints.max / 2);
                    const newHitPoints = changes.system.resources.hitPoints.value;
                    const previouslyAboveHalf = this.resources.hitPoints.value < halfHP;
                    const loweredBelowHalf = previouslyAboveHalf && newHitPoints >= halfHP;
                    const raisedAboveHalf = !previouslyAboveHalf && newHitPoints < halfHP;
                    if (loweredBelowHalf) {
                        await hordeActiveEffect.update({ disabled: false });
                    } else if (raisedAboveHalf) {
                        await hordeActiveEffect.update({ disabled: true });
                    }
                }
            }
        }
    }

    _onUpdate(changes, options, userId) {
        super._onUpdate(changes, options, userId);

        if (game.user.id === userId) {
            if (changes.system?.type) {
                const existingHordeEffect = this.parent.effects.find(x => x.type === 'horde');
                if (changes.system.type === CONFIG.DH.ACTOR.adversaryTypes.horde.id) {
                    if (!existingHordeEffect)
                        this.parent.createEmbeddedDocuments('ActiveEffect', [
                            {
                                type: 'horde',
                                name: game.i18n.localize('DAGGERHEART.CONFIG.AdversaryType.horde.label'),
                                img: 'icons/magic/movement/chevrons-down-yellow.webp',
                                disabled: true
                            }
                        ]);
                } else {
                    existingHordeEffect?.delete();
                }
            }
        }
    }

    prepareDerivedData() {
        super.prepareDerivedData();
        this.attack.roll.isStandardAttack = true;

        // Clamp resources (must be done last to ensure all updates occur)
        this.resources.clamp();
    }

    _getTags() {
        const tags = [
            game.i18n.localize(`DAGGERHEART.GENERAL.Tiers.${this.tier}`),
            `${game.i18n.localize(`DAGGERHEART.CONFIG.AdversaryType.${this.type}.label`)}`,
            `${game.i18n.localize('DAGGERHEART.GENERAL.difficulty')}: ${this.difficulty}`
        ];
        return tags;
    }

    /** Returns source data for this actor adjusted to a new tier, which can be used to create a new actor. */
    adjustForTier(tier) {
        const source = this.parent.toObject(true);

        /** @type {(2 | 3 | 4)[]} */
        const tiers = new Array(Math.abs(tier - this.tier))
            .fill(0)
            .map((_, idx) => idx + Math.min(tier, this.tier) + 1);
        if (tier < this.tier) tiers.reverse();
        const typeData = adversaryScalingData[source.system.type] ?? adversaryScalingData[source.system.standard];
        const tierEntries = tiers.map(t => ({ tier: t, ...typeData[t] }));

        // Apply simple tier changes
        const scale = tier > this.tier ? 1 : -1;
        for (const entry of tierEntries) {
            source.system.difficulty += scale * entry.difficulty;
            source.system.damageThresholds.major += scale * entry.majorThreshold;
            source.system.damageThresholds.severe += scale * entry.severeThreshold;
            source.system.resources.hitPoints.max += scale * entry.hp;
            source.system.resources.stress.max += scale * entry.stress;
            source.system.attack.roll.bonus += scale * entry.attack;
        }

        // Get the mean and standard deviation of expected damage in the previous and new tier
        // The data we have is for attack scaling, but we reuse this for action scaling later
        const expectedDamageData = adversaryExpectedDamage[source.system.type] ?? adversaryExpectedDamage.basic;
        const damageMeta = {
            currentDamageRange: { tier: source.system.tier, ...expectedDamageData[source.system.tier] },
            newDamageRange: { tier, ...expectedDamageData[tier] },
            type: 'attack'
        };

        // Update damage of base attack
        try {
            this.#adjustActionDamage(source.system.attack, damageMeta);
        } catch (err) {
            ui.notifications.warn('Failed to convert attack damage of adversary');
            console.error(err);
        }

        // Update damage of each item action, making sure to also update the description if possible
        const damageRegex = /@Damage\[([^\[\]]*)\]({[^}]*})?/g;
        for (const item of source.items) {
            // Replace damage inlines with new formulas
            for (const withDescription of [item.system, ...Object.values(item.system.actions)]) {
                withDescription.description = withDescription.description.replace(damageRegex, (match, inner) => {
                    const { value: formula } = parseInlineParams(inner);
                    if (!formula || !type) return match;

                    try {
                        const adjusted = this.#calculateAdjustedDamage(formula, { ...damageMeta, type: 'action' });
                        const newFormula = [
                            adjusted.diceQuantity ? `${adjusted.diceQuantity}d${adjusted.faces}` : null,
                            adjusted.bonus
                        ]
                            .filter(p => !!p)
                            .join('+');
                        return match.replace(formula, newFormula);
                    } catch {
                        return match;
                    }
                });
            }

            // Update damage in item actions
            // Parse damage, and convert all formula matches in the descriptions to the new damage
            for (const action of Object.values(item.system.actions)) {
                try {
                    const result = this.#adjustActionDamage(action, { ...damageMeta, type: 'action' });
                    if (!result) continue;

                    for (const { previousFormula, formula } of Object.values(result)) {
                        const oldFormulaRegexp = new RegExp(
                            previousFormula.replace(' ', '').replace('+', '(?:\\s)?\\+(?:\\s)?')
                        );
                        item.system.description = item.system.description.replace(oldFormulaRegexp, formula);
                        action.description = action.description.replace(oldFormulaRegexp, formula);
                    }
                } catch (err) {
                    ui.notifications.warn(`Failed to convert action damage for item ${item.name}`);
                    console.error(err);
                }
            }
        }

        // Finally set the tier of the source data, now that everything is complete
        source.system.tier = tier;
        return source;
    }

    /**
     * Converts a damage object to a new damage range
     * @returns {{ diceQuantity: number; faces: number; bonus: number }} the adjusted result as a combined term
     * @throws error if the formula is the wrong type
     */
    #calculateAdjustedDamage(formula, { currentDamageRange, newDamageRange, type }) {
        const terms = parseTermsFromSimpleFormula(formula);
        const flatTerms = terms.filter(t => t.diceQuantity === 0);
        const diceTerms = terms.filter(t => t.diceQuantity > 0);
        if (flatTerms.length > 1 || diceTerms.length > 1) {
            throw new Error('invalid formula for conversion');
        }
        const value = {
            ...(diceTerms[0] ?? { diceQuantity: 0, faces: 1 }),
            bonus: flatTerms[0]?.bonus ?? 0
        };
        const previousExpected = calculateExpectedValue(value);
        if (previousExpected === 0) return value; // nothing to do

        const dieSizes = [4, 6, 8, 10, 12, 20];
        const steps = newDamageRange.tier - currentDamageRange.tier;
        const increasing = steps > 0;
        const deviation = (previousExpected - currentDamageRange.mean) / currentDamageRange.deviation;
        const expected = Math.max(1, newDamageRange.mean + newDamageRange.deviation * deviation);

        // If this was just a flat number, convert to the expected damage and exit
        if (value.diceQuantity === 0) {
            value.bonus = Math.round(expected);
            return value;
        }

        const getExpectedDie = () => calculateExpectedValue({ diceQuantity: 1, faces: value.faces }) || 1;
        const getBaseAverage = () => calculateExpectedValue({ ...value, bonus: 0 });

        // Check the number of base overages over the expected die. In the end, if the bonus inflates too much, we add a die
        const baseOverages = Math.floor(value.bonus / getExpectedDie());

        // Prestep. Change number of dice for attacks, bump up/down for actions
        // We never bump up to d20, though we might bump down from it
        if (type === 'attack') {
            const minimum = increasing ? value.diceQuantity : 0;
            value.diceQuantity = Math.max(minimum, newDamageRange.tier);
        } else {
            const currentIdx = dieSizes.indexOf(value.faces);
            value.faces = dieSizes[Math.clamp(currentIdx + steps, 0, 4)];
        }

        value.bonus = Math.round(expected - getBaseAverage());

        // Attempt to handle negative values.
        // If we can do it with only step downs, do so. Otherwise remove tier dice, and try again
        if (value.bonus < 0) {
            let stepsRequired = Math.ceil(Math.abs(value.bonus) / value.diceQuantity);
            const currentIdx = dieSizes.indexOf(value.faces);

            // If step downs alone don't suffice, change the flat modifier, then calculate steps required again
            // If this isn't sufficient, the result will be slightly off. This is unlikely to happen
            if (type !== 'attack' && stepsRequired > currentIdx && value.diceQuantity > 0) {
                value.diceQuantity -= increasing ? 1 : Math.abs(steps);
                value.bonus = Math.round(expected - getBaseAverage());
                if (value.bonus >= 0) return value; // complete
            }

            stepsRequired = Math.ceil(Math.abs(value.bonus) / value.diceQuantity);
            value.faces = dieSizes[Math.max(0, currentIdx - stepsRequired)];
            value.bonus = Math.max(0, Math.round(expected - getBaseAverage()));
        }

        // If value is really high, we add a number of dice based on the number of overages
        // This attempts to preserve a similar amount of variance when increasing an action
        const overagesToRemove = Math.floor(value.bonus / getExpectedDie()) - baseOverages;
        if (type !== 'attack' && increasing && overagesToRemove > 0) {
            value.diceQuantity += overagesToRemove;
            value.bonus = Math.round(expected - getBaseAverage());
        }

        return value;
    }

    /**
     * Updates damage to reflect a specific value.
     * @throws if damage structure is invalid for conversion
     * @returns the converted formula and value as a simplified term, or null if it doesn't deal HP damage
     */
    #adjustActionDamage(action, damageMeta) {
        if (!action.damage?.parts.hitPoints) return null;

        const result = {};
        for (const property of ['value', 'valueAlt']) {
            const data = action.damage.parts.hitPoints[property];
            const previousFormula = data.custom.enabled
                ? data.custom.formula
                : [data.flatMultiplier ? `${data.flatMultiplier}${data.dice}` : 0, data.bonus ?? 0]
                      .filter(p => !!p)
                      .join('+');
            const value = this.#calculateAdjustedDamage(previousFormula, damageMeta);
            const formula = [value.diceQuantity ? `${value.diceQuantity}d${value.faces}` : null, value.bonus]
                .filter(p => !!p)
                .join('+');
            if (value.diceQuantity) {
                data.custom.enabled = false;
                data.bonus = value.bonus;
                data.dice = `d${value.faces}`;
                data.flatMultiplier = value.diceQuantity;
            } else if (!value.diceQuantity) {
                data.custom.enabled = true;
                data.custom.formula = formula;
            }

            result[property] = { previousFormula, formula, value };
        }

        return result;
    }
}
