"""Clipboard adapters for supported platforms."""

from __future__ import annotations

import base64
import shutil
import subprocess
import sys
from abc import ABC, abstractmethod

from cliproom.protocol import ClipboardFormat


SUPPORTED_MIME_TYPES = (
    "text/plain",
    "text/html",
    "image/png",
    "image/jpeg",
    "application/x-file-list",
)


class ClipboardUnavailableError(RuntimeError):
    """Raised when no compatible clipboard backend is available."""


class ClipboardAdapter(ABC):
    """Platform clipboard abstraction with multi-format support."""

    @abstractmethod
    def get_formats(self) -> list[ClipboardFormat]:
        """Read available clipboard formats in protocol representation."""

    @abstractmethod
    def set_formats(self, formats: list[ClipboardFormat]) -> None:
        """Write clipboard formats using best-effort MIME preference."""

    def get_text(self) -> str:
        for item in self.get_formats():
            if item.mime == "text/plain":
                return base64.b64decode(item.data).decode("utf-8")
        return ""

    def set_text(self, value: str) -> None:
        encoded = base64.b64encode(value.encode("utf-8")).decode("ascii")
        self.set_formats(
            [ClipboardFormat(mime="text/plain", encoding="utf-8", data=encoded)]
        )


def _to_clipboard_format(mime: str, payload: bytes, *, text: bool) -> ClipboardFormat:
    if text:
        encoded = base64.b64encode(payload).decode("ascii")
        return ClipboardFormat(mime=mime, encoding="utf-8", data=encoded)
    encoded = base64.b64encode(payload).decode("ascii")
    return ClipboardFormat(mime=mime, encoding="base64", data=encoded)


def _decode_clipboard_data(item: ClipboardFormat) -> bytes:
    return base64.b64decode(item.data)


class WindowsClipboardAdapter(ClipboardAdapter):
    def get_formats(self) -> list[ClipboardFormat]:
        formats: list[ClipboardFormat] = []

        plain = self._run_ps("Get-Clipboard -Raw")
        if plain is not None and plain != "":
            formats.append(_to_clipboard_format("text/plain", plain.encode("utf-8"), text=True))

        html = self._run_ps("Get-Clipboard -Format Html -Raw")
        if html:
            formats.append(_to_clipboard_format("text/html", html.encode("utf-8"), text=True))

        file_list = self._run_ps("(Get-Clipboard -Format FileDropList) -join \"`n\"")
        if file_list:
            formats.append(
                _to_clipboard_format(
                    "application/x-file-list",
                    file_list.encode("utf-8"),
                    text=True,
                )
            )

        png_b64 = self._run_ps(
            "$img=Get-Clipboard -Format Image;"
            "if($img){$ms=New-Object System.IO.MemoryStream;"
            "$img.Save($ms,[System.Drawing.Imaging.ImageFormat]::Png);"
            "[Convert]::ToBase64String($ms.ToArray())}"
        )
        if png_b64:
            with_bytes = base64.b64decode(png_b64)
            formats.append(_to_clipboard_format("image/png", with_bytes, text=False))

        return formats

    def set_formats(self, formats: list[ClipboardFormat]) -> None:
        by_mime = {f.mime: f for f in formats}

        if "image/png" in by_mime:
            payload = base64.b64encode(_decode_clipboard_data(by_mime["image/png"])).decode("ascii")
            script = (
                "$b=[Convert]::FromBase64String([Console]::In.ReadToEnd());"
                "$ms=New-Object IO.MemoryStream(,$b);"
                "$img=[System.Drawing.Image]::FromStream($ms);"
                "Set-Clipboard -Value $img"
            )
            self._run_ps_stdin(script, payload)
            return

        if "image/jpeg" in by_mime:
            payload = base64.b64encode(_decode_clipboard_data(by_mime["image/jpeg"])).decode("ascii")
            script = (
                "$b=[Convert]::FromBase64String([Console]::In.ReadToEnd());"
                "$ms=New-Object IO.MemoryStream(,$b);"
                "$img=[System.Drawing.Image]::FromStream($ms);"
                "Set-Clipboard -Value $img"
            )
            self._run_ps_stdin(script, payload)
            return

        if "text/html" in by_mime:
            text = _decode_clipboard_data(by_mime["text/html"]).decode("utf-8")
            self._run_ps_stdin("Set-Clipboard -AsHtml ([Console]::In.ReadToEnd())", text)
            return

        if "application/x-file-list" in by_mime:
            text = _decode_clipboard_data(by_mime["application/x-file-list"]).decode("utf-8")
            self._run_ps_stdin("Set-Clipboard -Value ([Console]::In.ReadToEnd())", text)
            return

        if "text/plain" in by_mime:
            text = _decode_clipboard_data(by_mime["text/plain"]).decode("utf-8")
            self._run_ps_stdin("Set-Clipboard -Value ([Console]::In.ReadToEnd())", text)
            return

        raise ClipboardUnavailableError("No supported MIME found in payload")

    def _run_ps(self, script: str) -> str | None:
        process = subprocess.run(
            ["powershell", "-NoProfile", "-Command", script],
            text=True,
            capture_output=True,
            check=False,
        )
        if process.returncode != 0:
            return None
        return process.stdout

    def _run_ps_stdin(self, script: str, value: str) -> None:
        process = subprocess.run(
            ["powershell", "-NoProfile", "-Command", script],
            text=True,
            input=value,
            capture_output=True,
            check=False,
        )
        if process.returncode != 0:
            raise ClipboardUnavailableError(process.stderr.strip() or "Set-Clipboard failed")


class WlClipboardAdapter(ClipboardAdapter):
    def get_formats(self) -> list[ClipboardFormat]:
        formats: list[ClipboardFormat] = []
        text = self._read_text("text/plain")
        if text:
            formats.append(_to_clipboard_format("text/plain", text, text=True))

        html = self._read_text("text/html")
        if html:
            formats.append(_to_clipboard_format("text/html", html, text=True))

        png = self._read_bytes("image/png")
        if png:
            formats.append(_to_clipboard_format("image/png", png, text=False))

        jpeg = self._read_bytes("image/jpeg")
        if jpeg:
            formats.append(_to_clipboard_format("image/jpeg", jpeg, text=False))

        file_list = self._read_text("text/uri-list")
        if file_list:
            formats.append(
                _to_clipboard_format("application/x-file-list", file_list, text=True)
            )
        return formats

    def set_formats(self, formats: list[ClipboardFormat]) -> None:
        by_mime = {f.mime: f for f in formats}
        for mime in ("image/png", "image/jpeg", "text/html", "application/x-file-list", "text/plain"):
            item = by_mime.get(mime)
            if item is None:
                continue
            data = _decode_clipboard_data(item)
            target = "text/uri-list" if mime == "application/x-file-list" else mime
            process = subprocess.run(
                ["wl-copy", "--type", target],
                input=data,
                capture_output=True,
                check=False,
            )
            if process.returncode != 0:
                raise ClipboardUnavailableError(
                    process.stderr.decode("utf-8", errors="ignore").strip()
                    or "wl-copy failed"
                )
            return
        raise ClipboardUnavailableError("No supported MIME found in payload")

    def _read_text(self, mime: str) -> bytes | None:
        process = subprocess.run(
            ["wl-paste", "--no-newline", "--type", mime],
            capture_output=True,
            check=False,
        )
        if process.returncode != 0 or not process.stdout:
            return None
        return process.stdout

    def _read_bytes(self, mime: str) -> bytes | None:
        process = subprocess.run(
            ["wl-paste", "--type", mime],
            capture_output=True,
            check=False,
        )
        if process.returncode != 0 or not process.stdout:
            return None
        return process.stdout


class XclipClipboardAdapter(ClipboardAdapter):
    def get_formats(self) -> list[ClipboardFormat]:
        targets = self._targets()
        formats: list[ClipboardFormat] = []

        if "UTF8_STRING" in targets or "text/plain" in targets:
            text = self._read("text/plain")
            if text:
                formats.append(_to_clipboard_format("text/plain", text, text=True))

        if "text/html" in targets:
            html = self._read("text/html")
            if html:
                formats.append(_to_clipboard_format("text/html", html, text=True))

        if "image/png" in targets:
            png = self._read("image/png")
            if png:
                formats.append(_to_clipboard_format("image/png", png, text=False))

        if "image/jpeg" in targets:
            jpeg = self._read("image/jpeg")
            if jpeg:
                formats.append(_to_clipboard_format("image/jpeg", jpeg, text=False))

        if "text/uri-list" in targets:
            file_list = self._read("text/uri-list")
            if file_list:
                formats.append(
                    _to_clipboard_format("application/x-file-list", file_list, text=True)
                )
        return formats

    def set_formats(self, formats: list[ClipboardFormat]) -> None:
        by_mime = {f.mime: f for f in formats}
        mapping = (
            ("image/png", "image/png"),
            ("image/jpeg", "image/jpeg"),
            ("text/html", "text/html"),
            ("application/x-file-list", "text/uri-list"),
            ("text/plain", "text/plain"),
        )
        for source_mime, target in mapping:
            item = by_mime.get(source_mime)
            if item is None:
                continue
            data = _decode_clipboard_data(item)
            process = subprocess.run(
                ["xclip", "-selection", "clipboard", "-t", target, "-in"],
                input=data,
                capture_output=True,
                check=False,
            )
            if process.returncode != 0:
                raise ClipboardUnavailableError(
                    process.stderr.decode("utf-8", errors="ignore").strip()
                    or "xclip write failed"
                )
            return
        raise ClipboardUnavailableError("No supported MIME found in payload")

    def _targets(self) -> set[str]:
        process = subprocess.run(
            ["xclip", "-selection", "clipboard", "-t", "TARGETS", "-o"],
            capture_output=True,
            check=False,
        )
        if process.returncode != 0:
            return set()
        return {
            line.strip()
            for line in process.stdout.decode("utf-8", errors="ignore").splitlines()
            if line.strip()
        }

    def _read(self, mime: str) -> bytes | None:
        process = subprocess.run(
            ["xclip", "-selection", "clipboard", "-t", mime, "-o"],
            capture_output=True,
            check=False,
        )
        if process.returncode != 0 or not process.stdout:
            return None
        return process.stdout


def create_default_adapter() -> ClipboardAdapter:
    """Auto-detect best clipboard adapter for the current platform."""
    if sys.platform.startswith("win"):
        return WindowsClipboardAdapter()

    if sys.platform.startswith("linux"):
        if shutil.which("wl-paste") and shutil.which("wl-copy"):
            return WlClipboardAdapter()
        if shutil.which("xclip"):
            return XclipClipboardAdapter()

    raise ClipboardUnavailableError("No supported clipboard backend found")
