"""Single-processing goals and reuse of the authored multi supply plan.

No input/coordinate implementation lives here. Receipts credit collected target
jobs, not registrations or intermediate work; stock deltas are output metrics,
never an exact-yield acceptance gate.
"""
from __future__ import annotations

from collections import Counter
from copy import deepcopy
from uuid import uuid4

from . import catalog
from .errors import require
from .multi_processing_plan import output_name, output_per_batch, plan_recipe_supply

MAX_SINGLE_TARGET = 999999


def normalize_goal(goal=None, goal_kind="runs", shortage_gather=False, navigate=False):
    require(goal_kind in ("runs", "quantity", "infinite", "register_only"), "가공 목표 단위가 올바르지 않습니다.")
    require(type(shortage_gather) is bool and type(navigate) is bool, "가공 실행 옵션 형식이 올바르지 않습니다.")
    if goal_kind == "register_only":
        # This is a single facility visit, not a production goal. Old form
        # values must not enable replenishment or create a hidden quota.
        goal, shortage_gather, navigate = None, False, True
    elif goal_kind == "infinite" or goal is None and goal_kind == "runs":
        goal, goal_kind = None, "infinite"
    else:
        require(type(goal) is int and 1 <= goal <= MAX_SINGLE_TARGET,
                f"가공 목표는 1~{MAX_SINGLE_TARGET:,} 사이의 정수여야 합니다.")
    return dict(goal=goal, goal_kind=goal_kind, shortage_gather=shortage_gather, navigate=navigate)


def work_amount(rows, recipe):
    name = catalog.ALTERING_NAMES.get(recipe["id"], recipe["name"])
    return sum(row.get("DisplayName") == name for row in rows)


def output_quantity(inventory, recipe):
    """Same observed bag/storage scope as the existing multi output metric."""
    return inventory_quantity(inventory, output_name(recipe))


def inventory_quantity(inventory, name):
    require(isinstance(inventory, list), "가공 산출품 재고 목록 미확인")
    rows = [row for row in inventory if isinstance(row, dict) and row.get("DisplayName") == name]
    require(all(row.get("Location") in ("inventory", "character_storage", "account_storage") and
                type(row.get("Count")) is int and row["Count"] >= 0 and
                type(row.get("IsLocked")) is bool for row in rows), "가공 산출품 수량/보관 위치 미확인: " + name)
    require(len({row.get("Category") for row in rows}) <= 1, "가공 산출품 분류 중복: " + name)
    return sum(row["Count"] for row in rows if not row["IsLocked"])


def registration_updates(receipt, rows, changed):
    owned = receipt.get("single_registration")
    if not owned:
        return {}
    counts = dict(owned["owned_before"])
    identifier = receipt["recipe"]
    counts[identifier] = min(counts.get(identifier, 0) + changed,
                             sum(row.get("DisplayName") == owned["display_name"] for row in rows))
    return dict(processing_owned=counts)


def collection_updates(receipt, rows, inventory_reader):
    """Idempotent totals from the durable before-snapshot, including recovery."""
    goal = receipt.get("single_goal")
    if not goal:
        return {}
    name = goal["display_name"]
    before = Counter((row.get("FacilityName"), row.get("DisplayName"))
                     for row in receipt.get("before_works", []))
    after = Counter((row.get("FacilityName"), row.get("DisplayName")) for row in rows)
    removed_names = Counter()
    for (_, display), count in (before - after).items():
        removed_names[display] += count
    owned = dict(goal.get("owned_before", {}))
    removed = min(owned.get(goal["recipe"]["id"], 0), removed_names[name])
    for identifier, display in goal.get("owned_names", {}).items():
        owned[identifier] = min(max(0, owned.get(identifier, 0) - removed_names[display]),
                                sum(row.get("DisplayName") == display for row in rows))
    completed = goal["target_collected_before"] + removed
    produced = goal["produced_before"]
    if removed and goal.get("output_before") is not None:
        produced += max(0, output_quantity(inventory_reader(), goal["recipe"]) - goal["output_before"])
    return dict(target_collected=completed, produced=produced, processing_owned=owned,
                completed=produced if goal["goal_kind"] == "quantity" else completed)


class SingleProcessingGoal:
    def __init__(self, service, recipe, options):
        self.service, self.recipe, self.options = service, recipe, options

    @property
    def run(self):
        return self.service.store.value["run"]

    @property
    def finite(self):
        return self.options["goal_kind"] in ("runs", "quantity")

    @property
    def register_only(self):
        return self.options["goal_kind"] == "register_only"

    @property
    def progress(self):
        if self.register_only:
            return int(self.run.get("registered", 0))
        return int(self.run.get("produced" if self.options["goal_kind"] == "quantity" else "target_collected", 0))

    @property
    def complete(self):
        if self.register_only:
            plan = self.run.get("processing_registration_pass")
            return plan is not None and self.progress >= plan["jobs"]
        return self.finite and self.progress >= self.options["goal"]

    def selected(self):
        stage = self.run.get("processing_stage")
        if not stage:
            return self.recipe
        rows = [row for row in catalog.processing_catalog()["recipes"] if row["id"] == stage["recipe_id"]]
        require(len(rows) == 1, "가공 재료 단계의 품목 기록 미확인")
        return rows[0]

    def supply_jobs(self, capacity):
        if not self.finite:
            return int(self.run.get("processing_preparation_jobs") or capacity)
        remaining = max(0, self.options["goal"] - self.progress)
        unit = output_per_batch(self.recipe) if self.options["goal_kind"] == "quantity" else 1
        return (remaining + unit - 1) // unit

    def requested_jobs(self, selected, rows, free):
        if selected["id"] != self.recipe["id"]:
            return min(free, max(0, self.run["processing_stage"]["jobs"] - work_amount(rows, selected)))
        if not self.finite:
            return free
        remaining = max(0, self.options["goal"] - self.progress)
        unit = output_per_batch(self.recipe) if self.options["goal_kind"] == "quantity" else 1
        needed = (remaining + unit - 1) // unit
        return min(free, max(0, needed - work_amount(rows, selected)))

    def collection_receipt(self, rows, port):
        owned = dict(self.run.get("processing_owned", {}))
        catalog_rows = {row["id"]: row for row in catalog.processing_catalog()["recipes"]}
        owned_names = {identifier: catalog.ALTERING_NAMES.get(identifier, catalog_rows[identifier]["name"])
                       for identifier in owned}
        target_jobs = work_amount(rows, self.recipe)
        return dict(recipe=self.recipe, goal_kind=self.options["goal_kind"],
                    owned_before=owned, owned_names=owned_names,
                    display_name=catalog.ALTERING_NAMES.get(self.recipe["id"], self.recipe["name"]),
                    target_collected_before=int(self.run.get("target_collected", 0)),
                    produced_before=int(self.run.get("produced", 0)),
                    output_before=(output_quantity(port.inventory(), self.recipe)
                        if self.options["goal_kind"] == "quantity" and
                        0 < target_jobs <= owned.get(self.recipe["id"], 0) else None))

    def registration_receipt(self, selected):
        return dict(owned_before=deepcopy(self.run.get("processing_owned", {})),
                    display_name=catalog.ALTERING_NAMES.get(selected["id"], selected["name"]))

    def shortage_plan(self, inventory, requested):
        """Whole remaining goal (or owned infinite pass), not the affordable prefix."""
        return plan_recipe_supply(self.recipe, inventory, requested)

    def start_stage(self, stage):
        self.service.store.set_run(processing_stage=dict(recipe_id=stage["recipe_id"], jobs=stage["jobs"]))

    def clear_stage(self):
        if self.run.get("processing_stage"):
            self.service.store.set_run(processing_stage=None)

    def supply(self, port, plan=None):
        supply = self.run.get("processing_supply")
        if supply is None:
            require(plan is not None and plan.get("gathers"), "부족 재료의 기존 채집 계획 미확인")
            supply = [dict(row, operation_id=uuid4().hex, done=False) for row in plan["gathers"]]
            self.service.store.set_run(processing_supply=supply)
        for entry in supply:
            if entry["done"]:
                continue
            nodes = [dict(node, skill=group["name"]) for group in catalog.gather_catalog()["groups"]
                     for node in group["nodes"] if node["id"] == entry["node_id"]]
            require(len(nodes) == 1, "부족 재료의 기존 채집 노드 미확인")
            self.service.status(f"가공 재료 보충 · {entry['item']} {entry['quantity']}개")
            result = port.gather(nodes[0], entry["item"], entry["quantity"], operation_id=entry["operation_id"])
            require(isinstance(result, dict) and type(result.get("acquired")) is int and
                    result["acquired"] >= entry["quantity"], "진행 중인 재료 채집을 같은 기록으로 재확인합니다.")
            entry["done"] = True
            self.service.store.set_run(processing_supply=supply)
        self.service.store.set_run(processing_supply=None)
        port.visit(self.selected())

    def result(self, port, state, reason=None):
        port.settle_collection_tail()
        if self.options["navigate"]:
            port._leave(recipe=self.selected())
        result = dict(state=state, completed=self.progress,
                      collected=int(self.run.get("target_collected", 0)),
                      produced=int(self.run.get("produced", 0)),
                      goal=self.options["goal"], goal_kind=self.options["goal_kind"])
        if self.register_only:
            result["registered"] = self.progress
            result["material_shortage"] = bool((self.run.get("processing_registration_pass") or {}).get("shortage"))
        if reason:
            result["reason"] = reason
        # `collected` remains the old facility-wide receipt counter in storage;
        # the returned count is only this task's owned target work.
        self.service.store.set_run(**{key: value for key, value in result.items() if key != "collected"})
        label = "등록 완료" if self.register_only and state == "complete" else ("목표 완료" if state == "complete" else "미완료")
        self.service.status(f"{self.recipe['name']} 가공 {label} · "
                            f"{self.progress}" + (f"/{self.options['goal']}" if self.finite else "") +
                            (f" · {reason}" if reason else ""))
        return result
