import aiohttp

from datetime import date
from typing import List, Literal

from . import models
from .news import urls_parser, urls_for_range, packages_parser, bugs_parser
from .packages import ftbfs_parser, watch_parser, watch_total_parser


class BaseParser:

    def __init__(self, session: aiohttp.ClientSession):
        self.session = session

    async def get(self, url: str, encoding: str = "utf-8"):
        async with self.session.get(url) as resp:
            resp.raise_for_status()
            return await resp.text(encoding=encoding)

    async def get_bytes(self, url: str) -> bytes:
        async with self.session.get(url) as resp:
            resp.raise_for_status()
            return await resp.read()


class NewsInfo:
    def __init__(self, client: BaseParser, cybertalk_url: str):
        self.client = client
        self._cybertalk_url = cybertalk_url

    async def news_urls(self) -> models.NewsURL:
        return await urls_parser(self.client, self._cybertalk_url)

    async def _get_packages(self, branch: str) -> models.PackagesModel | None:
        url = getattr(await self.news_urls(), branch, None)
        if not url:
            return None
        html = await self.client.get(url, "koi8-r")
        return await packages_parser(html, url, self.client)

    async def bugs(self) -> models.BugsModel | None:
        url = (await self.news_urls()).bugs
        if not url:
            return None
        html = await self.client.get(url, "koi8-r")
        return await bugs_parser(html, url)

    async def sisyphus(self) -> models.PackagesModel | None:
        return await self._get_packages("sisyphus")

    async def p11(self) -> models.PackagesModel | None:
        return await self._get_packages("p11")

    async def p10(self) -> models.PackagesModel | None:
        return await self._get_packages("p10")

    async def bugs_by_range(
        self, date_from: date, date_to: date
    ) -> dict[str, int] | None:
        urls = await urls_for_range(self.client, date_from, date_to, self._cybertalk_url, "bugs")
        if not urls:
            return None

        totals = {
            "quickly_resolved": 0, "new": 0, "old": 0,
            "resolved": 0, "reopened": 0, "random": 0,
        }

        for _, url in urls:
            html = await self.client.get(url, "koi8-r")
            data = await bugs_parser(html, url)
            if data and isinstance(data, models.BugsModel):
                for key in totals:
                    items = getattr(data, key, None)
                    if items:
                        totals[key] += len(items)

        if all(v == 0 for v in totals.values()):
            return None

        return totals

    async def packages_by_range(
        self, date_from: date, date_to: date
    ) -> models.PackagesModel | None:
        urls = await urls_for_range(self.client, date_from, date_to, self._cybertalk_url)
        if not urls:
            return None

        all_packages = []
        for _, url in urls:
            html = await self.client.get(url, "koi8-r")
            packages = await packages_parser(html, url, self.client)
            if packages and isinstance(packages, models.PackagesModel):
                all_packages.append(packages)

        if not all_packages:
            return None

        if len(all_packages) == 1:
            return all_packages[0]

        return _aggregate_packages(all_packages)


def _aggregate_packages(
    packages_list: list[models.PackagesModel],
) -> models.PackagesModel:
    latest_added = {}
    latest_removed = {}
    latest_updated = {}

    for packages in packages_list:
        for pkg in (packages.added or []):
            latest_added[pkg.name] = pkg
        for pkg in (packages.removed or []):
            latest_removed[pkg.name] = pkg
        for pkg in (packages.updated or []):
            latest_updated[pkg.name] = pkg

    # Добавлен + удалён → убираем из обоих
    cancelled = set(latest_added) & set(latest_removed)
    for name in cancelled:
        del latest_added[name]
        del latest_removed[name]

    # Добавлен + обновлён → оставляем в "добавлено" с последними данными
    for name in list(latest_updated):
        if name in latest_added:
            latest_added[name] = latest_updated.pop(name)
        elif name in latest_removed:
            del latest_updated[name]

    added = list(latest_added.values())
    removed = list(latest_removed.values())
    updated = list(latest_updated.values())

    return models.PackagesModel(
        url=packages_list[-1].url,
        total=packages_list[-1].total,
        added=added or None,
        removed=removed or None,
        updated=updated or None,
    )


class PackagesInfo:
    def __init__(self, client: BaseParser, ftbfs_url: str, watch_url: str, watch_total_url: str):
        self.client = client
        self._ftbfs_url = ftbfs_url
        self._watch_url = watch_url
        self._watch_total_url = watch_total_url

    async def ftbfs(self) -> List[models.FTBFSModel]:
        text = await self.client.get(self._ftbfs_url)
        return ftbfs_parser(text)

    async def watch_by_maintainer(
        self,
        maintainer_nickname: str,
        by_acl: Literal["by-acl", "by-expanded-acl", "by-expanded-leader", "by-leader"],
    ) -> List[models.WatchByMaintainerModel]:
        url = self._watch_url.format(by_acl=by_acl, nickname=maintainer_nickname)
        try:
            text = await self.client.get(url)
            return watch_parser(text)
        except:
            return []

    async def watch_total(self) -> List[models.WatchTotalModel]:
        text = await self.client.get(self._watch_total_url)
        return watch_total_parser(text)


class ALTRepoParser:
    def __init__(self, session: aiohttp.ClientSession, config: "ALTRepoConfig"):
        self._client = BaseParser(session)
        self.news = NewsInfo(self._client, config.cybertalk_url)
        self.packages = PackagesInfo(self._client, config.ftbfs_url, config.watch_url, config.watch_total_url)
