# retoor <retoor@molodetz.nl>

from __future__ import annotations

import json
from datetime import datetime, timedelta

from devplacepy.utils import generate_uid

from .. import economy
from .common import (
    GameError,
    PERK_COLUMN,
    clear_plot,
    conditional_update_farm,
    conditional_update_row,
    credit_farm,
    next_streak,
    raids_against_today,
    refund_farm,
    _iso,
    _iso_week,
    _lvl,
    _now,
    _parse,
    _plots,
    _quests,
    _steals,
    _today,
    steal_cooldown_remaining,
)
from .era import active_era_name
from .farm import _create_plot, _plot_at, ensure_farm, get_farm, get_plots
from .infrastructure import owns_infrastructure, roll_canary
from .market import market_factor_for, record_harvest_tick
from .mastery import MASTERY_COLUMN
from .quests import advance_quests, ensure_quests
from .serialize import _daily_available, _plot_state, _watered_by


def plant(user: dict, slot: int, crop_key: str) -> dict:
    farm = ensure_farm(user["uid"])
    crop = economy.crop_for(crop_key)
    if not crop:
        raise GameError("Unknown crop type.")
    progress = economy.level_progress(int(farm.get("xp", 0)))
    era_locked = bool(crop.era_key and crop.era_key != active_era_name())
    lock = economy.crop_lock_reason(
        crop, progress["level"], _lvl(farm, "mastery_points_earned_total"), era_locked
    )
    if lock:
        raise GameError(f"{crop.name} {lock[1]}.")
    plot = _plot_at(farm["uid"], slot)
    if not plot:
        raise GameError("That plot does not exist.")
    if plot.get("crop_key"):
        raise GameError("That plot is already in use.")
    coins = int(farm.get("coins", 0))
    cost = economy.effective_plant_cost(crop, _lvl(farm, "perk_discount"))
    if coins < cost:
        raise GameError("Not enough coins to plant that.")
    now = _now()
    ci_tier = int(farm.get("ci_tier", 1))
    grow = economy.grow_seconds_for(
        crop,
        ci_tier,
        _lvl(farm, "perk_growth"),
        _lvl(farm, "legacy_speed"),
        owns_infrastructure(farm, "registry"),
    )
    ready_at = now + timedelta(seconds=grow)
    charged = conditional_update_farm(
        farm["uid"], "coins = coins - :cost", "coins >= :cost", {"cost": cost}
    )
    if charged == 0:
        raise GameError("Not enough coins to plant that.")
    claimed = conditional_update_row(
        "game_plots",
        plot["uid"],
        "crop_key = :crop, planted_at = :planted, ready_at = :ready, watered_by = '[]', raided_fraction = 0",
        "crop_key = '' OR crop_key IS NULL",
        {"crop": crop.key, "planted": _iso(now), "ready": _iso(ready_at)},
    )
    if claimed == 0:
        refund_farm(farm["uid"], cost)
        raise GameError("That plot is already in use.")
    advance_quests(user["uid"], "plant", 1)
    return {"slot": slot, "crop": crop.key, "spent": cost}


def boost_flags(farm: dict, now) -> tuple[bool, bool]:
    underdog_until = _parse(farm.get("underdog_boost_until") or "")
    contract_until = _parse(farm.get("contract_boost_until") or "")
    return (
        bool(underdog_until) and now < underdog_until,
        bool(contract_until) and now < contract_until,
    )


def _harvest_crop(farm: dict, crop, plot_uid: str, planted_at: str, now, raided_fraction: float = 0.0) -> dict:
    golden = economy.is_golden(plot_uid, planted_at)
    underdog, contract_boost = boost_flags(farm, now)
    coins_gain = economy.realizable_harvest_coins(
        crop,
        _lvl(farm, "perk_yield"),
        _lvl(farm, "prestige"),
        _lvl(farm, "legacy_multiplier"),
        market_factor_for(crop.key),
        golden,
        contract_boost,
        underdog,
    )
    if owns_infrastructure(farm, "canary"):
        plant_cost = economy.effective_plant_cost(crop, _lvl(farm, "perk_discount"))
        coins_gain = roll_canary(coins_gain, plant_cost)
    remaining_share = max(0.0, 1.0 - max(0.0, min(1.0, raided_fraction)))
    coins_gain = int(coins_gain * remaining_share)
    xp_gain = economy.effective_reward_xp(crop, _lvl(farm, "perk_xp"))
    record_harvest_tick(crop.key, now)
    return {"coins": coins_gain, "xp": xp_gain, "golden": golden}


def harvest(user: dict, slot: int) -> dict:
    farm = ensure_farm(user["uid"])
    plot = _plot_at(farm["uid"], slot)
    if not plot or not plot.get("crop_key"):
        raise GameError("That plot is empty.")
    now = _now()
    if _plot_state(plot, now) != "ready":
        raise GameError("That build is still running.")
    crop = economy.crop_for(plot.get("crop_key", ""))
    if not crop:
        raise GameError("Unknown crop type.")
    planted_at = plot.get("planted_at", "")
    raided_fraction = float(plot.get("raided_fraction") or 0.0)
    if clear_plot(plot["uid"], planted_at) == 0:
        raise GameError("That plot is empty.")
    result = _harvest_crop(farm, crop, plot.get("uid", ""), planted_at, now, raided_fraction)
    coins_gain, xp_gain, golden = result["coins"], result["xp"], result["golden"]
    extra = {}
    if crop.key == "kernel" and _lvl(farm, "last_kernel_harvest_prestige") != _lvl(farm, "prestige"):
        prestiged_at = _parse(farm.get("prestiged_at") or "")
        if prestiged_at:
            extra["time_to_kernel_seconds"] = max(0, int((now - prestiged_at).total_seconds()))
        extra["last_kernel_harvest_prestige"] = _lvl(farm, "prestige")
    farm = credit_farm(
        farm,
        coins=coins_gain,
        xp=xp_gain,
        harvests=1,
        era_active=bool(active_era_name()),
        extra=extra or None,
    )
    advance_quests(user["uid"], "harvest", 1)
    advance_quests(user["uid"], "earn", coins_gain)
    if _lvl(farm, MASTERY_COLUMN["autoreplant"]):
        _try_autoreplant(farm, slot, crop, now)
    return {
        "slot": slot,
        "crop": crop.key,
        "coins": coins_gain,
        "xp": xp_gain,
        "golden": golden,
    }


def _try_autoreplant(farm: dict, slot: int, crop, now) -> None:
    plot = _plot_at(farm["uid"], slot)
    if not plot or plot.get("crop_key"):
        return
    cost = economy.effective_plant_cost(crop, _lvl(farm, "perk_discount"))
    if int(farm.get("coins", 0)) < cost:
        return
    grow = economy.grow_seconds_for(
        crop,
        int(farm.get("ci_tier", 1)),
        _lvl(farm, "perk_growth"),
        _lvl(farm, "legacy_speed"),
        owns_infrastructure(farm, "registry"),
    )
    ready_at = now + timedelta(seconds=grow)
    charged = conditional_update_farm(
        farm["uid"], "coins = coins - :cost", "coins >= :cost", {"cost": cost}
    )
    if charged == 0:
        return
    claimed = conditional_update_row(
        "game_plots",
        plot["uid"],
        "crop_key = :crop, planted_at = :planted, ready_at = :ready, watered_by = '[]', raided_fraction = 0",
        "crop_key = '' OR crop_key IS NULL",
        {"crop": crop.key, "planted": _iso(now), "ready": _iso(ready_at)},
    )
    if claimed == 0:
        refund_farm(farm["uid"], cost)


def water(visitor: dict, owner: dict, slot: int) -> dict:
    if visitor["uid"] == owner["uid"]:
        raise GameError("You cannot water your own build.")
    farm = ensure_farm(owner["uid"])
    plot = _plot_at(farm["uid"], slot)
    if not plot or not plot.get("crop_key"):
        raise GameError("That plot is empty.")
    now = _now()
    if _plot_state(plot, now) != "growing":
        raise GameError("That build is not running.")
    watered = _watered_by(plot)
    if visitor["uid"] in watered:
        raise GameError("You already watered this build.")
    if len(watered) >= economy.MAX_WATERS_PER_PLOT:
        raise GameError("This build has been watered enough.")
    crop = economy.crop_for(plot.get("crop_key", ""))
    ready_at = _parse(plot.get("ready_at", "")) or now
    bonus = economy.water_bonus_seconds(
        crop, int(farm.get("ci_tier", 1)), _lvl(farm, "perk_growth"), _lvl(farm, "legacy_speed")
    )
    if owns_infrastructure(farm, "registry") and crop.key in economy.REGISTRY_BOOST_CROPS:
        bonus = round(bonus * economy.REGISTRY_BOOST_FACTOR)
    new_ready = max(now, ready_at - timedelta(seconds=bonus))
    expected_watered = plot.get("watered_by") or "[]"
    watered.append(visitor["uid"])
    updated = conditional_update_row(
        "game_plots",
        plot["uid"],
        "ready_at = :ready, watered_by = :watered",
        "COALESCE(watered_by, '[]') = :expected AND crop_key = :crop",
        {
            "ready": _iso(new_ready),
            "watered": json.dumps(watered),
            "expected": expected_watered,
            "crop": plot.get("crop_key", ""),
        },
    )
    if updated == 0:
        raise GameError("This build was just watered - refresh and try again.")
    visitor_farm = ensure_farm(visitor["uid"])
    reward_coins = economy.water_reward_coins(
        _lvl(visitor_farm, "prestige"), _lvl(visitor_farm, "legacy_multiplier")
    )
    credit_farm(visitor_farm, coins=reward_coins, xp=economy.WATER_REWARD_XP)
    advance_quests(visitor["uid"], "water", 1)
    return {
        "slot": slot,
        "bonus_seconds": bonus,
        "reward_coins": reward_coins,
        "reward_xp": economy.WATER_REWARD_XP,
    }


def steal(thief: dict, owner: dict, slot: int) -> dict:
    if thief["uid"] == owner["uid"]:
        raise GameError("You cannot steal from your own farm.")
    farm = ensure_farm(owner["uid"])
    plot = _plot_at(farm["uid"], slot)
    if not plot or not plot.get("crop_key"):
        raise GameError("That plot is empty.")
    now = _now()
    if _plot_state(plot, now) != "ready":
        raise GameError("That build is not ready.")
    crop = economy.crop_for(plot.get("crop_key", ""))
    if not crop:
        raise GameError("Unknown crop type.")
    if crop.steal_immune:
        raise GameError("That build cannot be raided.")
    defense_level = _lvl(farm, "legacy_defense")
    building_tier = economy.defense_tier(_lvl(farm, "defense_level"))
    floor = building_tier.steal_fraction_floor
    cap = (
        economy.OBSERVABILITY_STEAL_CAP
        if owns_infrastructure(farm, "observability")
        else 1.0
    )
    grace = economy.effective_steal_grace(defense_level, building_tier.grace_bonus)
    ready_at = _parse(plot.get("ready_at", "")) or now
    if now < ready_at + timedelta(seconds=grace):
        raise GameError("That harvest is still protected.")
    raided_fraction = float(plot.get("raided_fraction") or 0.0)
    if raided_fraction >= 1.0:
        raise GameError("That build has already been stripped.")
    if int(economy.effective_reward_coins(crop) * (1.0 - raided_fraction)) < 1:
        raise GameError("There is nothing left worth taking from that build.")
    cooldown = steal_cooldown_remaining(thief["uid"], owner["uid"], now)
    if cooldown > 0:
        minutes = max(1, (cooldown + 59) // 60)
        raise GameError(
            f"You can only raid {owner.get('username', 'this farmer')} "
            f"once an hour. Try again in {minutes} min."
        )
    if raids_against_today(owner["uid"], now) >= economy.STEAL_MAX_PER_VICTIM_PER_DAY:
        raise GameError(
            f"{owner.get('username', 'This farmer')} has already been raided "
            f"{economy.STEAL_MAX_PER_VICTIM_PER_DAY} times today. Try again tomorrow."
        )
    fraction = economy.effective_steal_fraction(
        defense_level, floor, building_tier.steal_reduction, cap
    )
    share = min(fraction, 1.0 - raided_fraction)
    taken = conditional_update_row(
        "game_plots",
        plot["uid"],
        "raided_fraction = MIN(1.0, COALESCE(raided_fraction, 0) + :share)",
        "crop_key = :crop AND planted_at = :planted AND COALESCE(raided_fraction, 0) = :expected",
        {
            "share": share,
            "crop": crop.key,
            "planted": plot.get("planted_at", ""),
            "expected": raided_fraction,
        },
    )
    if taken == 0:
        raise GameError("That build just changed - refresh and try again.")
    market_factor = market_factor_for(crop.key)
    underdog_owner, contract_owner = boost_flags(farm, now)
    realizable = economy.realizable_harvest_coins(
        crop,
        _lvl(farm, "perk_yield"),
        _lvl(farm, "prestige"),
        _lvl(farm, "legacy_multiplier"),
        market_factor,
        economy.is_golden(plot.get("uid", ""), plot.get("planted_at", "")),
        contract_owner,
        underdog_owner,
    )
    available = int(realizable * (1.0 - raided_fraction))
    coins_gain = min(max(1, round(realizable * share)), available)
    thief_farm = ensure_farm(thief["uid"])
    underdog_extra = {}
    if int(farm.get("coins", 0)) > int(thief_farm.get("coins", 0)) * economy.UNDERDOG_COIN_RATIO:
        underdog_extra["underdog_boost_until"] = _iso(
            now + timedelta(hours=economy.UNDERDOG_DURATION_HOURS)
        )
    credit_farm(
        thief_farm,
        coins=coins_gain,
        era_active=bool(active_era_name()),
        extra=underdog_extra or None,
    )
    _steals().insert(
        {
            "uid": generate_uid(),
            "thief_uid": thief["uid"],
            "owner_uid": owner["uid"],
            "slot_index": slot,
            "crop_key": crop.key,
            "coins": coins_gain,
            "stolen_at": _iso(now),
            "created_at": _iso(now),
        }
    )
    return {
        "slot": slot,
        "crop": crop.key,
        "crop_name": crop.name,
        "coins": coins_gain,
        "owner_uid": owner["uid"],
        "share": round(share, 4),
        "underdog_triggered": bool(underdog_extra),
    }


def _auto_harvest(farm: dict, owner_uid: str, now: datetime) -> tuple[dict, dict]:
    coins_gain = 0
    xp_gain = 0
    harvested = 0
    replant_slots = []
    extra = {}
    for plot in get_plots(farm["uid"]):
        if _plot_state(plot, now) != "ready":
            continue
        crop = economy.crop_for(plot.get("crop_key", ""))
        if not crop:
            continue
        raided_fraction = float(plot.get("raided_fraction") or 0.0)
        if clear_plot(plot["uid"], plot.get("planted_at", "")) == 0:
            continue
        result = _harvest_crop(
            farm, crop, plot.get("uid", ""), plot.get("planted_at", ""), now, raided_fraction
        )
        coins_gain += result["coins"]
        xp_gain += result["xp"]
        harvested += 1
        replant_slots.append((int(plot.get("slot_index", 0)), crop))
        if crop.key == "kernel" and _lvl(farm, "last_kernel_harvest_prestige") != _lvl(
            farm, "prestige"
        ):
            prestiged_at = _parse(farm.get("prestiged_at") or "")
            if prestiged_at:
                extra["time_to_kernel_seconds"] = max(0, int((now - prestiged_at).total_seconds()))
            extra["last_kernel_harvest_prestige"] = _lvl(farm, "prestige")
    if not harvested:
        return farm, {"harvested": 0, "coins": 0, "xp": 0}
    farm = credit_farm(
        farm,
        coins=coins_gain,
        xp=xp_gain,
        harvests=harvested,
        era_active=bool(active_era_name()),
        extra=extra or None,
    )
    advance_quests(owner_uid, "harvest", harvested)
    advance_quests(owner_uid, "earn", coins_gain)
    if _lvl(farm, MASTERY_COLUMN["autoreplant"]):
        for slot_index, crop in replant_slots:
            _try_autoreplant(farm, slot_index, crop, now)
    summary = {"harvested": harvested, "coins": coins_gain, "xp": xp_gain}
    return (get_farm(owner_uid) or farm), summary


def claim_quest(user: dict, kind: str, scope: str = "daily") -> dict:
    from .quests import ensure_weekly_contract

    farm = ensure_farm(user["uid"])
    now = _now()
    if scope == "weekly":
        day = _iso_week(now)
        ensure_weekly_contract(farm, day)
    else:
        day = _today()
        ensure_quests(farm, day)
    row = _quests().find_one(farm_uid=farm["uid"], day=day, kind=kind, scope=scope)
    if not row:
        raise GameError("No such quest today.")
    if row.get("claimed"):
        raise GameError("Quest already claimed.")
    goal = int(row.get("goal") or 0)
    if int(row.get("progress") or 0) < goal:
        raise GameError("Quest not complete yet.")
    reward_coins = int(row.get("reward_coins") or 0)
    reward_xp = int(row.get("reward_xp") or 0)
    reward_stars = int(row.get("reward_stars") or 0)
    marked = conditional_update_row(
        "game_quests", row["uid"], "claimed = 1", "COALESCE(claimed, 0) = 0", {}
    )
    if marked == 0:
        raise GameError("Quest already claimed.")
    extra = None
    if scope == "weekly":
        extra = {
            "contract_boost_until": _iso(
                now + timedelta(hours=economy.WEEKLY_CONTRACT_BOOST_HOURS)
            )
        }
    credit_farm(farm, coins=reward_coins, xp=reward_xp, stars=reward_stars, extra=extra)
    return {
        "kind": kind,
        "reward_coins": reward_coins,
        "reward_xp": reward_xp,
        "reward_stars": reward_stars,
    }


def claim_daily(user: dict) -> dict:
    farm = ensure_farm(user["uid"])
    now = _now()
    if not _daily_available(farm, now):
        raise GameError("Daily bonus already claimed today.")
    streak = next_streak(farm.get("last_daily_at") or "", _lvl(farm, "streak"), now)
    reward = economy.daily_reward(
        streak,
        economy.social_reward_scale(
            _lvl(farm, "prestige"), _lvl(farm, "legacy_multiplier")
        ),
    )
    set_parts = [
        "coins = COALESCE(coins, 0) + :reward",
        "lifetime_coins_earned = COALESCE(lifetime_coins_earned, 0) + :reward",
        "streak = :streak",
        "last_daily_at = :now_iso",
    ]
    params = {
        "reward": reward,
        "streak": streak,
        "now_iso": _iso(now),
        "today": now.date().isoformat(),
    }
    if active_era_name():
        set_parts.append("era_coins = COALESCE(era_coins, 0) + :reward")
    rows = conditional_update_farm(
        farm["uid"],
        ", ".join(set_parts),
        "COALESCE(last_daily_at, '') = '' OR substr(last_daily_at, 1, 10) != :today",
        params,
    )
    if rows == 0:
        raise GameError("Daily bonus already claimed today.")
    return {"reward": reward, "streak": streak}


def upgrade_perk(user: dict, perk_key: str) -> dict:
    perk = economy.perk_for(perk_key)
    if not perk:
        raise GameError("Unknown perk.")
    farm = ensure_farm(user["uid"])
    column = PERK_COLUMN[perk.key]
    level = _lvl(farm, column)
    if level >= perk.max_level:
        raise GameError("That perk is maxed out.")
    cost = economy.perk_cost(perk, level)
    rows = conditional_update_farm(
        farm["uid"],
        set_clause=f"coins = coins - :cost, {column} = :new_level",
        where_clause=f"COALESCE({column}, 0) = :current_level AND coins >= :cost",
        params={"cost": cost, "new_level": level + 1, "current_level": level},
    )
    if rows == 0:
        farm = get_farm(user["uid"])
        if _lvl(farm, column) != level:
            raise GameError("That perk already changed - refresh and try again.")
        raise GameError("Not enough coins for that upgrade.")
    return {"perk": perk.key, "level": level + 1, "spent": cost}


def upgrade_legacy(user: dict, key: str) -> dict:
    upgrade = economy.legacy_for(key)
    if not upgrade:
        raise GameError("Unknown legacy upgrade.")
    farm = ensure_farm(user["uid"])
    column = f"legacy_{upgrade.key}"
    level = _lvl(farm, column)
    if level >= upgrade.max_level:
        raise GameError("That legacy upgrade is maxed out.")
    cost = economy.legacy_cost(upgrade, level)
    rows = conditional_update_farm(
        farm["uid"],
        set_clause=f"stars = COALESCE(stars, 0) - :cost, {column} = :new_level",
        where_clause=f"COALESCE({column}, 0) = :current_level AND COALESCE(stars, 0) >= :cost",
        params={"cost": cost, "new_level": level + 1, "current_level": level},
    )
    if rows == 0:
        farm = get_farm(user["uid"])
        if _lvl(farm, column) != level:
            raise GameError("That legacy upgrade already changed - refresh and try again.")
        raise GameError("Not enough stars for that legacy upgrade.")
    return {"key": upgrade.key, "level": level + 1, "spent": cost}


def prestige(user: dict) -> dict:
    from .treasury import credit_treasury

    farm = ensure_farm(user["uid"])
    level = economy.level_for_xp(int(farm.get("xp", 0)))
    if level < economy.PRESTIGE_MIN_LEVEL:
        raise GameError(
            f"Reach level {economy.PRESTIGE_MIN_LEVEL} to refactor (prestige)."
        )
    old_prestige = _lvl(farm, "prestige")
    new_prestige = old_prestige + 1
    coins_now = int(farm.get("coins", 0))
    fee = economy.refactor_cost(old_prestige, coins_now)
    if coins_now < fee:
        raise GameError(
            f"Refactoring costs {fee} coins right now - keep farming to afford it."
        )
    carried = economy.refactor_carryover(coins_now, fee, _lvl(farm, "legacy_carryover"))
    stars_award = economy.stars_for_refactor(level, old_prestige)
    mastery_award = economy.mastery_points_awarded(old_prestige, new_prestige)
    base_plots = economy.prestige_base_plots(_lvl(farm, "legacy_plots"))
    now_dt = _now()
    now = _iso(now_dt)

    set_parts = [
        "coins = :post_coins",
        "xp = 0",
        "level = 1",
        "ci_tier = 1",
        "plot_count = :base_plots",
        "prestige = :new_prestige",
        "stars = COALESCE(stars, 0) + :stars_award",
        "mastery_points = COALESCE(mastery_points, 0) + :mastery_award",
        "mastery_points_earned_total = COALESCE(mastery_points_earned_total, 0) + :mastery_award",
        "prestiged_at = :now",
    ]
    params = {
        "post_coins": economy.STARTING_COINS + carried,
        "base_plots": base_plots,
        "new_prestige": new_prestige,
        "stars_award": stars_award,
        "mastery_award": mastery_award,
        "now": now,
        "old_prestige": old_prestige,
        "coins_now": coins_now,
    }
    for perk in economy.PERKS:
        column = PERK_COLUMN[perk.key]
        set_parts.append(f"{column} = 0")

    rows = conditional_update_farm(
        farm["uid"],
        set_clause=", ".join(set_parts),
        where_clause="COALESCE(prestige, 0) = :old_prestige AND coins = :coins_now",
        params=params,
    )
    if rows == 0:
        farm = get_farm(user["uid"])
        if _lvl(farm, "prestige") != old_prestige:
            raise GameError("Your farm already refactored - refresh and try again.")
        if int(farm.get("coins", 0)) != coins_now:
            raise GameError("Your coin balance just changed - refresh and try again.")
        raise GameError(
            f"Reach level {economy.PRESTIGE_MIN_LEVEL} to refactor (prestige)."
        )
    credit_treasury(fee)

    kept_slots = set()
    for plot in get_plots(farm["uid"]):
        if int(plot.get("slot_index", 0)) >= base_plots:
            _plots().delete(uid=plot["uid"])
        else:
            kept_slots.add(int(plot.get("slot_index", 0)))
            _plots().update(
                {
                    "uid": plot["uid"],
                    "crop_key": "",
                    "planted_at": "",
                    "ready_at": "",
                    "watered_by": "[]",
                    "updated_at": now,
                },
                ["uid"],
            )
    for slot_index in range(base_plots):
        if slot_index not in kept_slots:
            _create_plot(farm["uid"], user["uid"], slot_index, now)
    return {
        "prestige": new_prestige,
        "stars_awarded": stars_award,
        "mastery_awarded": mastery_award,
        "fee": fee,
        "carried": carried,
    }


def fertilize(user: dict, slot: int) -> dict:
    farm = ensure_farm(user["uid"])
    plot = _plot_at(farm["uid"], slot)
    if not plot or not plot.get("crop_key"):
        raise GameError("That plot is empty.")
    now = _now()
    if _plot_state(plot, now) != "growing":
        raise GameError("That build is not running.")
    crop = economy.crop_for(plot.get("crop_key", ""))
    if not crop:
        raise GameError("Unknown crop type.")
    ready_at = _parse(plot.get("ready_at", "")) or now
    remaining = max(1, int((ready_at - now).total_seconds()))
    reduce_by = int(remaining * economy.FERTILIZE_FRACTION)
    if reduce_by < 1:
        raise GameError("That build is almost ready already.")
    full_grow = economy.grow_seconds_for(
        crop,
        int(farm.get("ci_tier", 1)),
        _lvl(farm, "perk_growth"),
        _lvl(farm, "legacy_speed"),
        owns_infrastructure(farm, "registry"),
    )
    underdog, contract_boost = boost_flags(farm, now)
    realizable = economy.realizable_harvest_coins(
        crop,
        _lvl(farm, "perk_yield"),
        _lvl(farm, "prestige"),
        _lvl(farm, "legacy_multiplier"),
        market_factor_for(crop.key),
        economy.is_golden(plot.get("uid", ""), plot.get("planted_at", "")),
        contract_boost,
        underdog,
        owns_infrastructure(farm, "canary"),
    )
    cost = economy.fertilize_click_cost(realizable, reduce_by, full_grow)
    charged = conditional_update_farm(
        farm["uid"], "coins = coins - :cost", "coins >= :cost", {"cost": cost}
    )
    if charged == 0:
        raise GameError("Not enough coins to fertilize.")
    new_ready = max(now, ready_at - timedelta(seconds=reduce_by))
    moved = conditional_update_row(
        "game_plots",
        plot["uid"],
        "ready_at = :new_ready",
        "ready_at = :expected AND crop_key = :crop",
        {
            "new_ready": _iso(new_ready),
            "expected": plot.get("ready_at", ""),
            "crop": plot.get("crop_key", ""),
        },
    )
    if moved == 0:
        refund_farm(farm["uid"], cost)
        raise GameError("That build just changed - refresh and try again.")
    return {"slot": slot, "spent": cost, "reduced_seconds": reduce_by}
