import re
from collections.abc import Callable, Sequence
from typing import Any

import httpx
import msgspec
from csirtmu.core.temporal import TemporalActivities
from temporalio import activity

from csirtmu.perun_sync.config import AliasSyncConfig, DatabaseConfig
from csirtmu.perun_sync.repository import PerunUserRepository


class User(msgspec.Struct):
    first_encountered_alias: str
    aliases: set[str] = set()
    other_emails: list[str] = []
    uco_at_mail_muni: str | None = None
    uco_at_muni: str | None = None
    uco: str | None = None


class AliasSyncActivities(TemporalActivities):
    def get_activities(self) -> Sequence[Callable[..., Any]]:
        return [self.sync]

    def __init__(self, config: AliasSyncConfig, db_config: DatabaseConfig) -> None:
        self.config = config
        self.db_config = db_config

    async def get_data(self) -> str:
        async with httpx.AsyncClient() as client:
            response = await client.get(self.config.url)
        return response.text

    def extract_user_data(self, data: str) -> list[str]:
        res = re.findall(
            r"\#\#\#\#\#\#\#\#\#\#\#\#\#\n\#\#\# STAFFS\#\#\#\n\#\#\#\#\#\#\#\#\#\#\#\#\#\n\#pro zamestnance, kteri nemaji ucet na dioru je nutno doplnit\n\#alias a radek ve tvaru\(uco velkyma\):\n"
            r"\#alias,jmeno,prijmeni,UVT,uco XXXX\n\#tyto radky se vygeneruji pomoci /usr/local/adm/pisalias\n\#ma\-li se zverejnovat alias misto loginu je nutno doplnit radek tvaru:\n\#alias: "
            r"uco:XXXX\n\#ma\-li se zverejnovat alias i login je nutno doplnit radek tvaru:\n\#alias: 2uco:XXXX\n\#priklad \- viz hojacova\n\#\n((?:^.+$\n)+?)\n\n",
            data,
            re.MULTILINE,
        )
        return res[0].split("\n")

    def parse_user_data(self, data: list[str]) -> tuple[dict[str, list[str]], dict[str, str]]:
        users: dict[str, list[str]] = {}
        ucos: dict[str, str] = {}
        for line in data:
            if line.startswith("#"):
                if (line_match := re.match(r"^\#[ ]?([\.\w]+)[\S: ,]+UCO[ :]+(\d+)$", line)) is not None:
                    alias, uco = line_match.groups()
                    if alias not in ucos:
                        ucos[alias] = uco
                    elif ucos[alias] != uco:
                        raise RuntimeError(f"Encountered alias with different UCO {ucos[alias]} != {uco}")
                elif (line_match := re.match(r"^\#[ ]?([\.\w@]+)[ :<>]+([\w@\.]+)$", line)) is not None:
                    alias, alias2 = line_match.groups()
                    if alias not in users:
                        users[alias] = [alias2]
                    else:
                        users[alias] += [alias2]
            else:
                if (line_match := re.match(r"^([\w@\.\+]+)[:?<> ]+([/\w@.\+]+)$", line)) is not None:
                    alias, fwd = line_match.groups()
                    if alias in users:
                        users[alias].append(fwd)
                    else:
                        users[alias] = [fwd]
        return users, ucos

    def check_user_uco(self, user: User, other: str | None) -> None:
        if user.uco is None:
            user.uco = other
        elif other is not None and user.uco != other:
            raise RuntimeError(f"MISMATCHED UCO FOUND {user.uco} != {other}")

    def _compare_and_set_if_none(self, match: re.Match[str], user: User, field_name: str, error: str) -> None:
        attribute = getattr(user, field_name)
        if attribute is not None:
            if not attribute == match.groups()[0]:
                raise RuntimeError(f"MISMATCHED {error} FOUND {attribute} != {match.groups()[0]}")
        else:
            setattr(user, field_name, match.groups()[0])

    def _parse_user_alias_data(self, user: User, alias: str, ucos: dict[str, str]) -> None:
        if (match := re.match(r"(([\d]+)@muni\.cz)", alias)) is not None:
            self._compare_and_set_if_none(match, user, "uco_at_muni", "UCO@muni.cz")
        elif (match := re.match(r"(([\d]+)@mail\.muni\.cz)", alias)) is not None:
            self._compare_and_set_if_none(match, user, "uco_at_mail_muni", "UCO@mail.muni.cz")
        elif "@" in alias:
            match = re.match(r"(([\d]+)@[\S]+muni\.cz)", alias)
            if alias not in user.other_emails:
                user.other_emails.append(alias)
        self.check_user_uco(user, ucos.get(alias))
        if match:
            self.check_user_uco(user, match.groups()[1])
        if user.uco is not None:
            user.uco_at_mail_muni = f"{user.uco}@mail.muni.cz"
            user.uco_at_muni = f"{user.uco}@muni.cz"

    def _compare_case_insensitive_search_for_email(self, key: str, alias: str) -> bool:
        return key == alias or key.lower() == alias.lower()

    def _get_alias_from_mail(self, alias: str) -> list[str]:
        if (match := re.match(r"^([\S]+)@[\S]*?muni\.cz$", alias)) is None:
            return []
        return [match.groups()[0]]

    def _get_mails_from_alias(self, alias: str) -> list[str]:
        if (match := re.match(r"^(\D[\w\.]+)$", alias)) is None:
            return []
        return [f"{match.groups()[0]}@ics.muni.cz", f"{match.groups()[0]}@muni.cz"]

    def build_user_data(self, users: dict[str, list[str]], ucos: dict[str, str]) -> list[User]:
        visited: set[str] = set()
        processed_users: list[User] = []
        for key, items in users.items():
            if key in visited:
                continue
            visited.add(key)
            user = User(
                first_encountered_alias=key,
                aliases={key, *items[:], *self._get_mails_from_alias(key), *self._get_alias_from_mail(key)},
            )
            aliases = set(items[:])
            while len(aliases) > 0:
                alias = aliases.pop()
                if alias in visited:
                    continue
                new_aliases = (
                    self._get_alias_from_mail(alias)
                    + self._get_mails_from_alias(alias)
                    + users.get(alias, [])
                    + users.get(alias.lower(), [])
                    + [
                        k
                        for k, v in users.items()
                        if self._compare_case_insensitive_search_for_email(k, alias)
                        or any(self._compare_case_insensitive_search_for_email(x, alias) for x in v)
                    ]
                )
                aliases.update(new_aliases)
                user.aliases.update(new_aliases)
                visited.add(alias)
            user.uco = ucos.get(key)
            for alias in user.aliases:
                self._parse_user_alias_data(user, alias, ucos)
            processed_users.append(user)
        return processed_users

    async def insert_to_db(self, data: list[User]) -> None:
        repo = PerunUserRepository(self.db_config)
        for user in data:
            if user.uco is not None and len(db_users := await repo.get_user_by_uco(user.uco)) > 0:
                for db_user in db_users:
                    db_user.aliases = list(user.aliases)
                await repo.save_changes(db_users)

    @activity.defn
    async def sync(self) -> None:
        data = await self.get_data()
        ext = self.extract_user_data(data)
        users, ucos = self.parse_user_data(ext)
        user_objects = self.build_user_data(users, ucos)
        await self.insert_to_db(user_objects)
