# ruff: noqa: F821, I001

from __future__ import annotations

import asyncio
from collections.abc import Iterator
from contextlib import AsyncExitStack
from pathlib import Path
from typing import TYPE_CHECKING

try: from graphics import Canvas
except ImportError: from cluster.user.graphics import Canvas

if TYPE_CHECKING: from router import Event

GENERATOR = "f4c64a8721d84c8388a31c0a6d7ed341"
PARAGRAPH = "Карточка собирается из отдельных слоёв. Текст переносится по словам, а размеры и порядок можно менять после создания.\nВторая строка начинается с нового абзаца."

class Poster:
    def __init__(self, assets: list[str]) -> None:
        self.assets = assets
        self.canvas = Canvas(1200, 940, "#11121a")

    def heading(self) -> None:
        hero = self.canvas.group("hero", 32, 32, 1136, 224)
        hero.image("hero-image", self.assets[0], 0, 0, 1136, 224, fit="cover", radius=20)
        hero.rect("hero-shade", 0, 0, 1136, 224, fill="#141020a8", radius=20)
        hero.image("hero-badge", self.assets[2], 32, 32, 64, 64, fit="contain")
        hero.text("heading", "Мастерская изображений", 120, 36, 970, 56, size=36, font="bold")
        hero.text("description", "Картинки, группы и сетки в одном модуле.\nКаждый элемент можно изменить по имени.", 120, 112, 970, 78, size=22, overflow="wrap", max_lines=3)
        hero.text("draft", "ЧЕРНОВИК", 930, 186, 170, 24, size=14, align="right", color="#cdb9ee")

    def cards(self) -> None:
        board = self.canvas.group("board", 32, 284, 1136, 512)
        for index in range(4): board.group(f"card-{index}", 0, 0, 1, 1)
        board.grid(2, gap=20)
        for index, card in enumerate(board.layers):
            group = board.get(card["id"])
            id, width, height = group.id, group.width, group.height
            group.rect(id + "/background", 0, 0, width, height, fill="#201c2c", radius=16)
            group.image(id + "/image", self.assets[index % 3], 16, 16, 150, height - 32, fit=("cover", "contain", "stretch", "cover")[index])
            group.text(id + "/title", ("Обрезка по центру", "Изображение целиком", "Растяжение", "Копирование группы")[index], 188, 20, width - 208, 58, size=21, font="bold", overflow="wrap", max_lines=2)
            group.text(id + "/body", PARAGRAPH, 188, 92, width - 208, height - 108, size=17, overflow="wrap", max_lines=5, color="#c7bed7")

    def footer(self) -> None:
        footer = self.canvas.group("footer", 32, 824, 1136, 84)
        for index in range(3):
            footer.group("stat-" + str(index), 0, 0, 1, 1)
        footer.row(gap=16)
        for index in range(3):
            group = footer.get("stat-" + str(index))
            group.rect(f"stat-bg-{index}", 0, 0, group.width, group.height, fill="#32274280", radius=14)
            group.text(f"stat-label-{index}", ("3 исходных картинки", "4 карточки", "До и после изменения")[index], 16, 24, group.width - 32, 32, size=19, align="center")

    def build(self) -> Canvas:
        self.heading()
        self.cards()
        self.footer()
        return self.canvas

    @staticmethod
    def edit(canvas: Canvas, asset: str) -> Canvas:
        changed = canvas.copy()
        changed.get("heading").value("Макет после изменений").set(color="#d8c2ff")
        changed.get("description").value("Заменены текст и картинка. Карточка скопирована,\nслои переставлены, подпись скрыта.")
        changed.get("hero-image").set(asset=asset, fit="stretch")
        changed.get("draft").hide()
        board = changed.get("board")
        copied = board.get("card-1").copy("featured")
        copied.get("featured/card-1/title").value("Копия второй карточки")
        copied.get("featured/card-1/body").value("Независимая копия. Изменение этого текста не затронет исходную сцену.")
        copied.get("featured/card-1/image").set(fit="cover").opacity(0.75)
        copied.get("featured/card-1/background").fill("#352547")
        copied.get("featured/card-1/title").front()
        copied.get("featured/card-1/background").back()
        board.remove("card-1")
        board.order("card-3", "featured", "card-0", "card-2").grid(2, gap=20)
        return changed

    @staticmethod
    def compact(canvas: Canvas) -> Canvas:
        changed = Canvas.from_dict(canvas.build())
        board = changed.get("board")
        board.row(gap=17)
        for item in board.layers:
            group, id = board.get(item["id"]), item["id"]
            group.get(id + "/background").resize(group.width, group.height)
            group.get(id + "/image").at(16, 16).resize(group.width - 32, 180)
            group.get(id + "/title").at(16, 218).resize(group.width - 32, 72)
            group.get(id + "/body").at(16, 306).resize(group.width - 32, 180).set(max_lines=7)
        changed.get("heading").value("Тот же макет в четыре колонки")
        return changed


class Module:
    handler_timeout = 180

    def __init__(self) -> None: self.lock = asyncio.Lock()

    async def process(self) -> dict:
        async with self.lock, AsyncExitStack() as stack:
            renderer = app.images(GENERATOR)
            assets = []
            for name in ("landscape.png", "portrait.jpg", "badge.webp"):
                raw = Path(__file__).with_name(name).read_bytes()
                file = await stack.enter_async_context(await app.files.store(name, raw, ttl=120))
                asset = await stack.enter_async_context(await renderer.upload(file))
                assets.append(asset.id)
            canvas = Poster(assets).build()
            original = canvas.build()
            edited = Poster.edit(canvas, assets[1])
            compact = Poster.compact(canvas)
            if canvas.build() != original: raise ValueError("Editing a copy changed the original scene")
            measured = await renderer.measure(PARAGRAPH, size=17, width=348, height=138, overflow="wrap", max_lines=5)
            if not 1 < len(measured["lines"]) <= 5 or measured["width"] > 348: raise ValueError("Invalid wrapped text measurements")
            files = []
            try:
                for name, scene in (("before", canvas), ("after", edited), ("compact", compact), ("repeat", Canvas.from_dict(original))):
                    file = await renderer.render(scene, name + ".png")
                    files.append(file)
                hashes = [file.data["sha256"] for file in files]
                if hashes[0] != hashes[3] or len(set(hashes[:3])) != 3: raise ValueError("The scene changed unexpectedly")
                return {"files": [{"id": file.id, "name": file.data["filename"], "sha256": file.data["sha256"], "size": file.data["size"]} for file in files],
                        "measurement": measured, "layers": sum(1 for _ in self.layers(original["layers"])), "original_preserved": True}
            except BaseException:
                for file in files: await file.delete()
                raise

    @staticmethod
    def layers(items: list) -> Iterator[dict]:
        for item in items:
            yield item
            yield from Module.layers(item.get("layers", []))

    @loader.action(access="owner")
    async def compose(self, ctx: Event) -> dict: return await self.process()

    @loader.action(access="owner")
    async def reject(self, ctx: Event) -> dict:
        renderer = app.images(GENERATOR)
        async with await renderer.upload(b"not an image") as asset:
            canvas = renderer.canvas(100, 100)
            canvas.image("broken", asset.id, 0, 0, 100, 100)
            try: await renderer.render(canvas)
            except RuntimeError as error:
                if getattr(error, "code", None) == "invalid_input" or error.args == ("invalid_input",): return {"rejected": True}
                raise
            raise ValueError("A corrupt image was accepted")
