import asyncio
import datetime
import random
import re
from contextlib import asynccontextmanager

import nanoid
from pydantic import BaseModel, Field, ValidationError

from konabot.plugins.kona_ph.core.path import KONAPH_DATA_JSON


class PuzzleHint(BaseModel):
    pattern: str
    message: str
    is_checkpoint: bool


class PuzzleSubmission(BaseModel):
    success: bool
    flag: str
    time: datetime.datetime
    hint_id: int = -1


class Puzzle(BaseModel):
    raw_id: str
    "用于给出题者管理的 ID"

    index_id: str
    "展出的 ID，以展出顺序为准"

    title: str
    content: str
    img_name: str
    author_id: str
    flag: str

    ready: bool = False
    created_at: datetime.datetime = Field(default_factory=datetime.datetime.now)

    hints: dict[int, PuzzleHint] = Field(default_factory=dict)

    @property
    def hint_id_max(self) -> int:
        return max((0, *self.hints.keys()))

    def check_submission(
        self,
        submission: str,
        time: datetime.datetime | None = None,
    ) -> PuzzleSubmission:
        if time is None:
            time = datetime.datetime.now()
        if submission == self.flag:
            return PuzzleSubmission(
                success=True,
                flag=submission,
                time=time,
            )
        for hint_id, hint in self.hints.items():
            if hint.pattern.startswith('/') and hint.pattern.endswith('/'):
                if re.match(hint.pattern.strip('/'), submission):
                    return PuzzleSubmission(
                        success=False,
                        flag=submission,
                        time=time,
                        hint_id=hint_id,
                    )
            else:
                if hint.pattern == submission:
                    return PuzzleSubmission(
                        success=False,
                        flag=submission,
                        time=time,
                        hint_id=hint_id,
                    )
        return PuzzleSubmission(
            success=False,
            flag=submission,
            time=time,
        )


class DailyPuzzleInfo(BaseModel):
    raw_id: str
    time: datetime.date
    tried_users: set[str] = set()
    success_users: dict[str, datetime.datetime] = {}


class PuzzleSubmissionFeedback(BaseModel):
    submission: PuzzleSubmission
    puzzle: Puzzle
    info: DailyPuzzleInfo | None = None


def get_today_date() -> datetime.date:
    now = datetime.datetime.now()
    if now.hour < 8:
        now -= datetime.timedelta(days=1)
    return now.date()


class PuzzleManager(BaseModel):
    puzzle_data: dict[str, Puzzle] = {}

    daily_puzzle: dict[str, DailyPuzzleInfo] = {}
    daily_puzzle_of_date: dict[datetime.date, str] = {}

    puzzle_pinned: str = ""

    index_id_counter: int = 1
    submissions: dict[str, dict[str, list[PuzzleSubmission]]] = {}
    last_checked_date: datetime.date = Field(
        default_factory=lambda: get_today_date() - datetime.timedelta(days=1)
    )

    @property
    def last_publish_date(self):
        return max(self.daily_puzzle_of_date.keys())

    @property
    def unpublished_puzzles(self):
        return set((
            p.raw_id for p in self.puzzle_data.values()
            if not self.is_puzzle_published(p.raw_id) and p.ready
        ))

    @property
    def unready_puzzles(self):
        return set((
            p.raw_id for p in self.puzzle_data.values()
            if not self.is_puzzle_published(p.raw_id) and not p.ready
        ))

    @property
    def published_puzzles(self):
        return set((
            p.raw_id for p in self.puzzle_data.values()
            if self.is_puzzle_published(p.raw_id)
        ))

    def is_puzzle_published(self, raw_id: str):
        return raw_id in [i.raw_id for i in self.daily_puzzle.values()]

    def publish_puzzle(self, raw_id: str):
        assert raw_id in self.puzzle_data

        today = get_today_date()

        p = self.puzzle_data[raw_id]
        p.index_id = str(self.index_id_counter)
        p.ready = True
        self.puzzle_pinned = ""
        self.last_checked_date = today
        self.daily_puzzle[p.index_id] = DailyPuzzleInfo(
            raw_id=raw_id,
            time=today,
        )
        self.daily_puzzle_of_date[today] = p.index_id

        self.index_id_counter += 1

    def admin_pin_puzzle(self, raw_id: str):
        if raw_id in self.puzzle_data:
            self.puzzle_pinned = raw_id
        else:
            self.puzzle_pinned = ""

    def get_today_puzzle(self, strong: bool = False) -> Puzzle | None:
        today = get_today_date()
        if today in self.daily_puzzle_of_date:
            index_id = self.daily_puzzle_of_date[today]
            info = self.daily_puzzle[index_id]
            return self.puzzle_data[info.raw_id]
        if today == self.last_checked_date and not strong:
            return
        self.last_checked_date = today
        if self.puzzle_pinned and self.puzzle_pinned in self.puzzle_data:
            d = self.puzzle_pinned
            self.publish_puzzle(d)
            self.puzzle_pinned = ""
            return self.puzzle_data[d]
        elif len(self.unpublished_puzzles) > 0:
            d = random.choice(list(self.unpublished_puzzles))
            self.publish_puzzle(d)
            return self.puzzle_data[d]

    def get_today_info(self) -> DailyPuzzleInfo | None:
        p = self.get_today_puzzle()
        if p is None:
            return
        return self.daily_puzzle[p.index_id]

    def submit(self, user: str, flag: str) -> PuzzleSubmissionFeedback | str:
        p = self.get_today_puzzle()
        d = self.get_today_info()
        now = datetime.datetime.now()
        if p is None or d is None:
            return "今天没有题哦，改天再来吧！"
        if user in d.success_users:
            return "你今天已经答对过啦！不用重复提交哦！"
        d.tried_users.add(user)
        result = p.check_submission(flag, now)
        self.submissions.setdefault(p.raw_id, {}).setdefault(user, []).append(result)
        if result.success:
            d.success_users[user] = now
        return PuzzleSubmissionFeedback(
            submission=result,
            puzzle=p,
            info=d,
        )

    def admin_create_puzzle(self, user: str):
        p = Puzzle(
            raw_id=nanoid.generate(
                "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz",
                12,
            ),
            index_id="",
            title="示例标题",
            content="题目的内容填写内容",
            img_name="",
            author_id=user,
            flag="konaph{this_is_a_flag}",
            ready=False,
        )
        self.puzzle_data[p.raw_id] = p
        return p

    def get_puzzles_of_user(self, user: str):
        return sorted([
            p for p in self.puzzle_data.values()
            if p.author_id == user
        ], key=lambda p: p.created_at, reverse=True)


lock = asyncio.Lock()


def read_data():
    try:
        data_raw = KONAPH_DATA_JSON.read_text("utf-8")
        return PuzzleManager.model_validate_json(data_raw)
    except (FileNotFoundError, ValidationError):
        return PuzzleManager()


def write_data(data: PuzzleManager):
    KONAPH_DATA_JSON.write_text(data.model_dump_json(), "utf-8")


@asynccontextmanager
async def puzzle_manager():
    async with lock:
        data = read_data()
        yield data
        write_data(data)
