"""Application service container and startup/shutdown lifecycle."""

from __future__ import annotations

import logging
from dataclasses import dataclass, field
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError

from app.config import Settings
from app.database.repositories import Repositories
from app.database.session import Database
from app.gateways.base import BotIdentity, TelegramGateway
from app.services.admin_service import AdminService
from app.services.channel_service import ChannelService
from app.services.delivery_service import DeliveryService
from app.services.media_service import MediaService
from app.services.membership_service import MembershipService
from app.services.post_service import PostService
from app.services.publishing_service import PublishingService
from app.services.rate_limit_service import RateLimitService
from app.services.scheduler_service import SchedulerService
from app.services.settings_service import SettingsService
from app.services.statistics_service import StatisticsService
from app.services.token_service import TokenService

logger = logging.getLogger(__name__)


@dataclass(slots=True)
class AppContext:
    settings: Settings
    db: Database
    repos: Repositories
    gateway: TelegramGateway
    app_settings: SettingsService
    admins: AdminService
    media: MediaService
    tokens: TokenService
    membership: MembershipService
    rate_limit: RateLimitService
    publishing: PublishingService
    posts: PostService
    delivery: DeliveryService
    scheduler: SchedulerService
    channels: ChannelService
    statistics: StatisticsService
    bot_username: str | None = None
    _closed: bool = field(default=False, init=False)

    async def close(self) -> None:
        if self._closed:
            return
        self._closed = True
        try:
            await self.scheduler.stop()
        finally:
            await self.db.dispose()


async def build_context(
    settings: Settings,
    gateway: TelegramGateway,
    *,
    identity: BotIdentity | None = None,
) -> AppContext:
    settings.ensure_directories()
    db = Database(settings.database_url)
    if settings.auto_create_schema:
        await db.create_schema()
    repos = Repositories(db)
    app_settings = SettingsService(
        repos,
        timezone=settings.timezone,
        max_caption_length=settings.max_caption_length,
    )
    await app_settings.ensure_defaults()
    # The database value is the source of truth after the first run; the env
    # value only seeds a fresh installation. This lets the panel change the
    # display timezone without it being overwritten on every restart.
    stored_timezone = await app_settings.timezone()
    try:
        ZoneInfo(stored_timezone)
    except ZoneInfoNotFoundError:
        logger.warning(
            "database timezone %r is invalid; restoring the validated env timezone %r",
            stored_timezone,
            settings.timezone,
        )
        await app_settings.set("timezone", settings.timezone)
    else:
        settings.timezone = stored_timezone
    admins = AdminService(repos)
    await admins.seed(settings.admin_ids)

    username = settings.bot_username
    if not username and identity is not None:
        # getMe is authoritative for the currently running token; a stale DB
        # value must not keep generating links for an old bot username.
        username = identity.username
    if not username:
        username = await app_settings.bot_username()
    if username:
        await app_settings.set("bot_username", username)

    tokens = TokenService(repos)
    membership = MembershipService(repos, gateway)
    rate_limit = RateLimitService(repos)
    publishing = PublishingService(repos, settings, app_settings, gateway, tokens)
    posts = PostService(repos, app_settings, tokens, publishing)
    delivery = DeliveryService(
        repos,
        settings,
        app_settings,
        gateway,
        tokens,
        membership,
        rate_limit,
    )
    scheduler = SchedulerService(repos, settings, publishing, delivery)
    media = MediaService(repos, settings, gateway)
    channels = ChannelService(repos, gateway)
    statistics = StatisticsService(repos, app_settings)

    return AppContext(
        settings=settings,
        db=db,
        repos=repos,
        gateway=gateway,
        app_settings=app_settings,
        admins=admins,
        media=media,
        tokens=tokens,
        membership=membership,
        rate_limit=rate_limit,
        publishing=publishing,
        posts=posts,
        delivery=delivery,
        scheduler=scheduler,
        channels=channels,
        statistics=statistics,
        bot_username=username,
    )
