from __future__ import annotations

import asyncio
import json
import re
import time
from dataclasses import dataclass
from typing import Any

import aiohttp
from loguru import logger

from .config import settings


class VKAPIError(RuntimeError):
    def __init__(self, code: int | None, message: str) -> None:
        self.code = code
        super().__init__(f"VK API error {code}: {message}")


class VKRateLimiter:
    def __init__(self, rps: int = 3) -> None:
        self.rps = max(1, int(rps))
        self.interval = 1.0 / self.rps
        self._last = 0.0
        self._lock = asyncio.Lock()

    async def acquire(self) -> None:
        async with self._lock:
            now = time.monotonic()
            wait = self.interval - (now - self._last)
            if wait > 0:
                await asyncio.sleep(wait)
            self._last = time.monotonic()


class VKAPIClient:
    base_url = "https://api.vk.com/method"

    def __init__(
        self,
        token: str | None = None,
        version: str | None = None,
        rps: int = 3,
        timeout_total_sec: int = 60,
        timeout_connect_sec: int = 10,
        rate_limit_sleep_sec: float = 1.0,
        retry_attempts: int = 3,
        retry_min_delay_sec: float = 2.0,
        retry_max_delay_sec: float = 10.0,
    ) -> None:
        self.token = token or settings.vk_access_token
        self.version = version or settings.vk_api_version
        self.limiter = VKRateLimiter(rps)
        self.timeout_total_sec = max(1, int(timeout_total_sec))
        self.timeout_connect_sec = max(1, int(timeout_connect_sec))
        self.rate_limit_sleep_sec = max(0.1, float(rate_limit_sleep_sec))
        self.retry_attempts = max(1, int(retry_attempts))
        self.retry_min_delay_sec = max(0.1, float(retry_min_delay_sec))
        self.retry_max_delay_sec = max(self.retry_min_delay_sec, float(retry_max_delay_sec))
        self.session: aiohttp.ClientSession | None = None

    async def __aenter__(self) -> "VKAPIClient":
        self.session = aiohttp.ClientSession(
            timeout=aiohttp.ClientTimeout(total=self.timeout_total_sec, connect=self.timeout_connect_sec)
        )
        return self

    async def __aexit__(self, *args) -> None:
        if self.session:
            await self.session.close()

    async def call(self, method: str, **params: Any) -> dict | list:
        if not self.session:
            raise RuntimeError("VKAPIClient is not initialized")
        if not self.token:
            raise RuntimeError("VK_ACCESS_TOKEN is empty")

        payload = dict(params)
        payload["access_token"] = self.token
        payload["v"] = self.version

        for attempt in range(1, self.retry_attempts + 1):
            try:
                await self.limiter.acquire()
                async with self.session.post(f"{self.base_url}/{method}", data=payload) as resp:
                    resp.raise_for_status()
                    data = await resp.json(content_type=None)
                if "error" not in data:
                    return data.get("response", {})

                err = data["error"]
                code = err.get("error_code")
                message = err.get("error_msg", "unknown")
                if code == 6 and attempt < self.retry_attempts:
                    logger.warning("VK rate limit hit, sleeping {}s", self.rate_limit_sleep_sec)
                    await asyncio.sleep(self.rate_limit_sleep_sec)
                    continue
                raise VKAPIError(code, message)
            except VKAPIError:
                raise
            except Exception:
                if attempt >= self.retry_attempts:
                    raise
                delay = min(self.retry_min_delay_sec * (2 ** (attempt - 1)), self.retry_max_delay_sec)
                await asyncio.sleep(delay)

        raise VKAPIError(None, "retry exhausted")

    async def resolve_group(self, input_value: str) -> tuple[str, int, str]:
        external_id = normalize_vk_source(input_value)
        if external_id.lstrip("-").isdigit():
            group_id = abs(int(external_id))
            response = await self.call("groups.getById", group_id=str(group_id))
        else:
            response = await self.call("groups.getById", group_id=external_id)
        items = response if isinstance(response, list) else response.get("groups", [])
        if not items:
            raise VKAPIError(None, f"cannot resolve VK group: {input_value}")
        group = items[0]
        group_id = int(group["id"])
        screen_name = str(group.get("screen_name") or external_id)
        name = str(group.get("name") or screen_name)
        return screen_name, -group_id, name

    async def get_wall_posts(self, owner_id: int, count: int, offset: int = 0) -> dict:
        response = await self.call("wall.get", owner_id=owner_id, count=count, offset=offset, filter="owner")
        if not isinstance(response, dict):
            raise VKAPIError(None, "wall.get returned non-object response")
        return response

    async def get_wall_upload_server(self, group_id: int) -> str:
        response = await self.call("photos.getWallUploadServer", group_id=abs(int(group_id)))
        if not isinstance(response, dict) or not response.get("upload_url"):
            raise VKAPIError(None, "photos.getWallUploadServer returned no upload_url")
        return str(response["upload_url"])

    async def upload_wall_photo_bytes(self, group_id: int, data: bytes, filename: str = "photo.jpg") -> dict:
        if not self.session:
            raise RuntimeError("VKAPIClient is not initialized")
        upload_url = await self.get_wall_upload_server(group_id)
        form = aiohttp.FormData()
        form.add_field("photo", data, filename=filename, content_type="image/jpeg")
        async with self.session.post(upload_url, data=form) as resp:
            uploaded = await resp.json(content_type=None)
        saved = await self.call(
            "photos.saveWallPhoto",
            group_id=abs(int(group_id)),
            photo=uploaded.get("photo"),
            server=uploaded.get("server"),
            hash=uploaded.get("hash"),
        )
        if not isinstance(saved, list) or not saved:
            raise VKAPIError(None, f"photos.saveWallPhoto returned invalid response: {json.dumps(saved)[:300]}")
        return saved[0]

    async def upload_wall_photo_url(self, group_id: int, url: str) -> dict:
        if not self.session:
            raise RuntimeError("VKAPIClient is not initialized")
        async with self.session.get(url) as resp:
            resp.raise_for_status()
            content_type = resp.headers.get("content-type") or "image/jpeg"
            data = await resp.read()
        suffix = ".jpg"
        if "png" in content_type:
            suffix = ".png"
        elif "webp" in content_type:
            suffix = ".webp"
        return await self.upload_wall_photo_bytes(group_id, data, filename=f"photo{suffix}")

    async def create_wall_post(
        self,
        owner_id: int,
        message: str,
        attachments: list[str],
        from_group: bool = True,
    ) -> int:
        response = await self.call(
            "wall.post",
            owner_id=int(owner_id),
            from_group=1 if from_group else 0,
            message=message or "",
            attachments=",".join(attachments) if attachments else "",
        )
        if not isinstance(response, dict) or "post_id" not in response:
            raise VKAPIError(None, f"wall.post returned invalid response: {response}")
        return int(response["post_id"])


def normalize_vk_source(value: str) -> str:
    raw = str(value or "").strip()
    if not raw:
        return ""
    if "vk.com" in raw:
        raw = raw.split("vk.com", 1)[1]
    raw = raw.lstrip("/")
    raw = raw.split("?", 1)[0].split("#", 1)[0].strip()
    raw = re.sub(r"^(club|public)", "", raw, flags=re.IGNORECASE)
    return raw.lower()


def is_repost(post: dict) -> bool:
    return bool(post.get("copy_history"))


def is_deleted_or_invalid(post: dict) -> bool:
    if post.get("is_deleted") or post.get("deleted"):
        return True
    if not post.get("id") or not post.get("date"):
        return True
    return False


def is_fatal_source_error(error: Exception | str) -> bool:
    message = str(error or "").lower()
    fatal_patterns = (
        "vk api error 15:",
        "vk api error 18:",
        "vk api error 19:",
        "vk api error 100:",
        "vk api error 113:",
        "vk api error 1051:",
        "vk api error 200:",
        "vk api error 201:",
        "vk api error 203:",
        "[15]",
        "[18]",
        "[19]",
        "[100]",
        "[113]",
        "[1051]",
        "[200]",
        "[201]",
        "[203]",
        "access denied",
        "private profile",
        "cannot resolve vk group",
        "cannot resolve screen_name",
        "method is unavailable with current profile type",
        "wall is disabled",
    )
    return any(pattern in message for pattern in fatal_patterns)


def post_vk_url(owner_id: int, post_id: int | str) -> str:
    return f"https://vk.com/wall{int(owner_id)}_{post_id}"


@dataclass
class ExtractedMedia:
    media_type: str
    original_url: str | None
    attachment_id: str
    width: int | None = None
    height: int | None = None
    duration_sec: int | None = None
    sort_order: int = 0


def extract_media(post: dict) -> list[ExtractedMedia]:
    out: list[ExtractedMedia] = []
    for idx, att in enumerate(post.get("attachments", []) or []):
        att_type = att.get("type")
        if att_type == "photo":
            photo = att.get("photo") or {}
            sizes = sorted(
                photo.get("sizes", []) or [],
                key=lambda s: int(s.get("width") or 0) * int(s.get("height") or 0),
                reverse=True,
            )
            best = sizes[0] if sizes else {}
            owner_id = photo.get("owner_id")
            media_id = photo.get("id")
            if owner_id is None or media_id is None:
                continue
            out.append(
                ExtractedMedia(
                    media_type="photo",
                    original_url=best.get("url"),
                    attachment_id=f"photo{owner_id}_{media_id}",
                    width=best.get("width"),
                    height=best.get("height"),
                    sort_order=idx,
                )
            )
        elif att_type == "video":
            video = att.get("video") or {}
            owner_id = video.get("owner_id")
            media_id = video.get("id")
            if owner_id is None or media_id is None:
                continue
            out.append(
                ExtractedMedia(
                    media_type="video",
                    original_url=f"https://vk.com/video{owner_id}_{media_id}",
                    attachment_id=f"video{owner_id}_{media_id}",
                    width=video.get("width"),
                    height=video.get("height"),
                    duration_sec=video.get("duration"),
                    sort_order=idx,
                )
            )
    return out
