import datetime
import logging
from collections.abc import Awaitable, Callable, Sequence
from datetime import timedelta
from typing import Any

import msgspec
import structlog
from csirtmu.core.temporal.config import create_temporal_client
from redis import Redis
from temporalio import activity, workflow

from csirtmu.beta.redis_structs import RedisStruct, TransactionStateEnum
from csirtmu.beta_temporal.commons.actions import MattermostActions
from csirtmu.beta_temporal.config import AppConfig, TemporalConfig
from csirtmu.beta_temporal.constants import WorkflowReference
from csirtmu.beta_temporal.external_services.mattermost.client import MattermostClient
from csirtmu.beta_temporal.workflows.logging import LoggedActivities, LoggedScheduledWorkflow

logger: logging.Logger = structlog.getLogger()


class RetryFailedTransactionsActivities(LoggedActivities):
    def __init__(self, temporal_config: TemporalConfig, redis: "Redis[bytes]") -> None:
        self.temporal_config = temporal_config
        self.redis = redis

    def get_activities(self) -> Sequence[Callable[..., Any]]:
        return [self.check_redis_db_size, self.process_failed_transactions]

    @activity.defn
    async def check_redis_db_size(self) -> int:
        self.log_activity()
        return self.redis.dbsize()

    @activity.defn
    async def process_failed_transactions(self) -> None:
        self.log_activity()
        tc = await create_temporal_client(self.temporal_config, self.temporal_config.periodic_tasks_queue)
        successful_retries: list[RedisStruct] = []
        # Fetch keys from redis in batches of 1k
        for key in self.redis.scan_iter(count=1000):
            # Fetch value from redis and check whether transaction is already processing.
            if (data := self.redis.get(key)) is None or (
                redis_struct := msgspec.json.decode(data, type=RedisStruct)
            ).state == TransactionStateEnum.PROCESSING:
                continue
            # Set transaction state to processing.
            redis_struct.state = TransactionStateEnum.PROCESSING
            self.redis.set(key, msgspec.json.encode(redis_struct))
            # Retry transaction handling workflow and mark as successful or not.
            try:
                await tc.start_workflow(
                    WorkflowReference.HANDLE_TRANSACTION,
                    args=(redis_struct.transaction, redis_struct.workflows),
                    id=f"{redis_struct.transaction.id}|{redis_struct.transaction.type.name}",
                    task_queue=redis_struct.task_queue,
                )
                successful_retries.append(redis_struct)
            except Exception as e:
                logger.exception(e)
                # Set unsuccessful transaction as pending.
                redis_struct.state = TransactionStateEnum.PENDING
                self.redis.set(key, msgspec.json.encode(redis_struct))
        # Delete successful transactions from redis.
        if successful_retries:
            self.redis.delete(*[str(t.transaction.id) for t in successful_retries])


class RetryFailedTransactions(LoggedScheduledWorkflow):
    @classmethod
    def get_schedule_interval(cls) -> timedelta:
        return datetime.timedelta(minutes=AppConfig.get().transaction_retention.retry_workflow_interval_minutes)

    @classmethod
    def get_schedule_args(cls) -> tuple[Any, ...]:
        return ()

    @classmethod
    def get_schedule_task_queue(cls) -> str:
        return AppConfig.get().temporal.periodic_tasks_queue

    @classmethod
    def get_activities(cls) -> Sequence[Callable[..., Awaitable[Any]]]:
        config = AppConfig.get()
        redis = Redis(
            config.redis.host, config.redis.port, username=config.redis.username, password=config.redis.password
        )
        mattermost = MattermostActions(config.rt.base_url, MattermostClient(config.mattermost))
        return [
            *RetryFailedTransactionsActivities(config.temporal, redis).get_activities(),
            *mattermost.get_activities(),
        ]

    @workflow.run
    async def run(self) -> None:
        self.log_workflow()
        config = AppConfig.get()
        if (
            count := await workflow.execute_activity(
                RetryFailedTransactionsActivities.check_redis_db_size,
                retry_policy=config.temporal.retry_policy,
                start_to_close_timeout=config.temporal.activity_timeout,
            )
        ) > 0:
            await workflow.execute_activity(
                MattermostActions.notify_mattermost_on_failed_transactions,
                args=(config.transaction_retention.mm_channel, count),
                retry_policy=config.temporal.retry_policy,
                start_to_close_timeout=config.temporal.activity_timeout,
            )
            await workflow.execute_activity(
                RetryFailedTransactionsActivities.process_failed_transactions,
                args=(),
                retry_policy=config.temporal.retry_policy,
                start_to_close_timeout=config.temporal.activity_timeout,
            )
