"""Classes and methods to control configuration of fedibooster."""

import re
import sys
from collections.abc import Callable
from dataclasses import dataclass
from dataclasses import field
from io import StringIO
from pathlib import Path
from typing import Any

import msgspec
from diskcache import Cache
from httpx import URL
from httpx import AsyncClient
from loguru import logger as log
from minimal_activitypub.client_2_server import ActivityPub

from fedibooster import USER_AGENT

CONFIG_CACHE_EXPIRE = 3600  # one hour


class Fediverse(msgspec.Struct):
    """Config values for Fediverse account to cross post to."""

    domain_name: str
    api_token: str
    search_instance: str = ""


@dataclass
class NoReblog:
    """Lists of strings and regex patterns to no re-blog."""

    tags_list: list[str] = field(default_factory=list)
    tags_regexs: list[re.Pattern] = field(default_factory=list)
    users_list: list[str] = field(default_factory=list)
    users_regexes: list[re.Pattern] = field(default_factory=list)


class Configuration(msgspec.Struct):
    """Config for bot."""

    fediverse: Fediverse
    run_continuously: bool
    delay_between_posts: int
    search_tags_list: list[str]
    reblog_sensitive: bool = False
    max_reblog: int = 5
    reblog_langs: list[str] | None = None
    max_tags: int | None = None
    no_reblog_users_list: list[str] | None = None
    no_reblog_users_regex: list[str] | None = None
    no_reblog_tags_list: list[str] | None = None
    no_reblog_tags_regex: list[str] | None = None

    async def load_external_configs(self, client: AsyncClient, cache: Cache) -> tuple[NoReblog, list[str], list[str]]:
        """Load externally hosted configuration into NoReblog and search tags into list[str]."""
        no_reblog = NoReblog()

        search_tags: list[str] = await load_external_config_list(
            file_locations=self.search_tags_list,
            client=client,
            cache=cache,
        )
        reblog_langs: list[str] = await load_external_config_list(
            file_locations=self.reblog_langs,
            client=client,
            cache=cache,
        )
        no_reblog.users_list = await load_external_config_list(
            file_locations=self.no_reblog_users_list,
            client=client,
            cache=cache,
        )
        no_reblog.tags_list = await load_external_config_list(
            file_locations=self.no_reblog_tags_list,
            client=client,
            cache=cache,
        )
        no_reblog.users_regexes = await load_external_config_regex(
            file_locations=self.no_reblog_users_regex,
            client=client,
            cache=cache,
        )
        no_reblog.tags_regexs = await load_external_config_regex(
            file_locations=self.no_reblog_tags_regex,
            client=client,
            cache=cache,
        )

        return no_reblog, search_tags, reblog_langs


async def determine_config(config_path: Path) -> Configuration:
    """Determine configuration to use."""
    if config_path.exists():
        with config_path.open(mode="rb") as config_file:
            config_content = config_file.read()
            config = msgspec.toml.decode(config_content, type=Configuration)

    else:
        config = await create_default_config()
        log.debug(f"{config=}")
        with config_path.open(mode="wb") as config_file:
            config_file.write(msgspec.toml.encode(config))
        print("Please review your config file, adjust as needed, and run fedibooster again.")
        sys.exit(0)

    return config


async def create_default_config() -> Configuration:
    """Create default configuration."""
    domain_name = input("Please enter the url for your Fediverse instance: ")

    async with AsyncClient(http2=True, timeout=30) as client:
        client_id, client_secret = await ActivityPub.create_app(
            instance_url=domain_name,
            user_agent=USER_AGENT,
            client=client,
        )
        auth_url = await ActivityPub.generate_authorization_url(
            instance_url=domain_name,
            client_id=client_id,
            user_agent=USER_AGENT,
        )

        print("Please go to the following URL and follow the prompts to authorize fedibooster to use your account:")
        print(f"{auth_url}")
        auth_code = input("Please provide the authorization token provided by your instance: ")

        auth_token = await ActivityPub.validate_authorization_code(
            client=client,
            instance_url=domain_name,
            authorization_code=auth_code,
            client_id=client_id,
            client_secret=client_secret,
        )

    return Configuration(
        fediverse=Fediverse(
            domain_name=domain_name,
            api_token=auth_token,
            search_instance="mastodon.social",
        ),
        run_continuously=False,
        delay_between_posts=300,
        max_reblog=5,
        search_tags_list=[
            "https://codeberg.org/marvinsmastodontools/fedibooster-lists/raw/branch/main/tags/search.lst"
        ],
    )


async def _load_external_config(
    file_locations: list[str] | None,
    client: AsyncClient,
    cache: Cache,
    line_processor: Callable[[str], Any],
) -> list[Any]:
    """Load external configuration files.

    Reads text files line-by-line and returns a list of processed items.
    Blank lines and lines starting with '#' are ignored.
    """
    results: list[Any] = []

    if not file_locations:
        return results

    for location in file_locations:
        file_location = URL(location)

        # Try to get from cache first
        if cached_results := cache.get(file_location):
            log.debug(f"Re-loaded '{file_location}' from cache.")
            results.extend(cached_results)
            continue

        # Fetch from remote source
        source_response = await client.get(url=file_location, follow_redirects=True)
        source_response.raise_for_status()
        content = source_response.text.replace("\r\n", "\n")

        location_results: list[Any] = []

        for line in StringIO(content):
            clean_line = line.rstrip("\n")
            if not clean_line or clean_line.startswith("#"):
                continue

            try:
                processed_item = line_processor(clean_line)
                location_results.append(processed_item)
            except (re.error, ValueError) as exc:
                raise ValueError(f"Invalid line {line!r}: {exc}") from None

        # Cache the results for this location
        cache.add(file_location, location_results, expire=CONFIG_CACHE_EXPIRE)
        log.debug(f"Added '{file_location}' to cache.")

        results.extend(location_results)

    return results


async def load_external_config_regex(
    file_locations: list[str] | None, client: AsyncClient, cache: Cache
) -> list[re.Pattern]:
    """Read a text file line-by-line and return a list of compiled
    `re.Pattern` objects, one per non-empty line.
    Blank lines and lines starting with '#' are ignored.
    """

    def compile_regex(line: str) -> re.Pattern:
        """Compile a regex pattern from a line."""
        return re.compile(line)

    return await _load_external_config(
        file_locations=file_locations,
        client=client,
        cache=cache,
        line_processor=compile_regex,
    )


async def load_external_config_list(file_locations: list[str] | None, client: AsyncClient, cache: Cache) -> list[str]:
    """Read a text file line-by-line and return a list of strings,
    one per non-empty line.
    Blank lines and lines starting with '#' are ignored.
    """

    def store_string(line: str) -> str:
        """Store the line as a string."""
        return line

    return await _load_external_config(
        file_locations=file_locations,
        client=client,
        cache=cache,
        line_processor=store_string,
    )
