from __future__ import annotations

import calendar
import hashlib
import hmac
import json
import re
import secrets
import sqlite3
import unicodedata
from datetime import UTC, date, datetime, timedelta
from math import ceil
from zoneinfo import ZoneInfo

from backend.database.connection import Database
from backend.errors import BusinessError
from backend.features.accounts.models import (
    BirthProfile,
    MembershipAccountView,
    MembershipRecord,
    MembershipView,
    Principal,
    SessionIssue,
    UserRecord,
)
from backend.features.accounts.repository import AccountRepository
from backend.llm.repository import LLMRepository
from backend.security import PasswordHasher, PasswordPolicyError, SecretCipher

USERNAME_PATTERN = re.compile(r"[A-Za-z0-9_\-\u4e00-\u9fff]{3,30}")
SHANGHAI = ZoneInfo("Asia/Shanghai")
PROFILE_PRIVACY_NOTICE = "原始出生资料加密保存，仅当前账号可见。"
SESSION_LIFETIME = timedelta(days=30)


def now_utc() -> datetime:
    return datetime.now(UTC)


def username_key(username: str) -> str:
    return unicodedata.normalize("NFKC", username).casefold()


def normalize_username(raw_username: str) -> tuple[str, str]:
    username = unicodedata.normalize("NFKC", raw_username.strip())
    if not USERNAME_PATTERN.fullmatch(username):
        raise BusinessError(
            "invalid_username", "账号名应为3至30位中文、字母、数字、下划线或连字符。"
        )
    return username, username_key(username)


def hash_token(token: str) -> str:
    return hashlib.sha256(token.encode("utf-8")).hexdigest()


def add_months(value: datetime, months: int) -> datetime:
    month_index = value.year * 12 + value.month - 1 + months
    year, zero_based_month = divmod(month_index, 12)
    month = zero_based_month + 1
    day = min(value.day, calendar.monthrange(year, month)[1])
    return value.replace(year=year, month=month, day=day)


def membership_active(membership: MembershipRecord, now: datetime) -> bool:
    return membership.state == "active" and (
        membership.is_permanent
        or (membership.expires_at is not None and membership.expires_at > now)
    )


def membership_view(
    user: UserRecord,
    membership: MembershipRecord,
    used_today: int,
    now: datetime,
) -> MembershipView:
    active = membership_active(membership, now)
    if membership.state == "disabled":
        status = "disabled"
    elif active:
        status = "active"
    else:
        status = "not_open"
    remaining_days = None
    if active and not membership.is_permanent and membership.expires_at is not None:
        remaining_days = max(0, ceil((membership.expires_at - now).total_seconds() / 86400))
    quota_exempt = user.is_admin
    return MembershipView(
        status=status,
        active=active,
        is_permanent=membership.is_permanent,
        expires_at=membership.expires_at if not membership.is_permanent else None,
        remaining_days=remaining_days,
        daily_limit=membership.daily_llm_limit,
        used_today=used_today,
        remaining_today=(None if quota_exempt else max(0, membership.daily_llm_limit - used_today)),
        quota_exempt=quota_exempt,
    )


class AccountService:
    def __init__(
        self,
        database: Database,
        repository: AccountRepository,
        password_hasher: PasswordHasher,
        cipher: SecretCipher,
    ) -> None:
        self._database = database
        self._repository = repository
        self._password_hasher = password_hasher
        self._cipher = cipher
        self._dummy_password_hash = password_hasher.hash("not-a-real-account-123!")

    def register(self, raw_username: str, password: str) -> SessionIssue:
        username, normalized_key = normalize_username(raw_username)
        try:
            password_hash = self._password_hasher.hash(password)
        except PasswordPolicyError as exc:
            raise BusinessError("invalid_password", str(exc)) from exc
        now = now_utc()
        try:
            with self._database.transaction() as connection:
                is_admin = self._repository.count_users(connection) == 0
                user = self._repository.create_user(
                    connection, username, normalized_key, password_hash, is_admin, now
                )
                membership = self._repository.create_default_membership(connection, user.id, now)
                return self._issue_session(connection, user, membership, now)
        except sqlite3.IntegrityError as exc:
            raise BusinessError("username_taken", "该账号名已被使用。") from exc

    def login(self, raw_username: str, password: str) -> SessionIssue:
        normalized_key = username_key(raw_username.strip())
        now = now_utc()
        with self._database.transaction() as connection:
            user = self._repository.get_user_by_username_key(connection, normalized_key)
            encoded_hash = user.password_hash if user else self._dummy_password_hash
            password_valid = self._password_hasher.verify(password, encoded_hash)
            if user is None or not password_valid or user.status != "active":
                raise BusinessError("invalid_credentials", "账号或密码错误。")
            membership = self._repository.get_membership(connection, user.id)
            if membership is None:
                raise BusinessError("account_unavailable", "账号暂不可用，请联系管理员。")
            return self._issue_session(connection, user, membership, now)

    def authenticate(self, raw_token: str | None) -> Principal | None:
        if not raw_token:
            return None
        token_hash = hash_token(raw_token)
        now = now_utc()
        with self._database.read() as connection:
            session = self._repository.get_session(connection, token_hash)
        if session is None:
            return None
        if session.expires_at <= now or session.user.status != "active":
            with self._database.transaction() as connection:
                self._repository.delete_session(connection, token_hash)
            return None
        return Principal(
            token_hash=session.token_hash,
            csrf_hash=session.csrf_hash,
            user=session.user,
            membership=session.membership,
        )

    @staticmethod
    def verify_csrf(
        principal: Principal,
        header_token: str | None,
        cookie_token: str | None,
    ) -> bool:
        if not header_token or not cookie_token:
            return False
        if not hmac.compare_digest(header_token, cookie_token):
            return False
        return hmac.compare_digest(hash_token(header_token), principal.csrf_hash)

    def logout(self, principal: Principal) -> None:
        with self._database.transaction() as connection:
            self._repository.delete_session(connection, principal.token_hash)

    def change_password(
        self,
        principal: Principal,
        current_password: str,
        new_password: str,
        confirmation: str,
    ) -> None:
        if new_password != confirmation:
            raise BusinessError("password_mismatch", "两次输入的新密码不一致。")
        if not self._password_hasher.verify(current_password, principal.user.password_hash):
            raise BusinessError("invalid_current_password", "当前密码不正确。")
        try:
            new_hash = self._password_hasher.hash(new_password)
        except PasswordPolicyError as exc:
            raise BusinessError("invalid_password", str(exc)) from exc
        now = now_utc()
        with self._database.transaction() as connection:
            self._repository.update_password(connection, principal.user.id, new_hash, now)
            self._repository.delete_other_sessions(
                connection, principal.user.id, principal.token_hash
            )

    def get_profile(self, user_id: int) -> BirthProfile | None:
        with self._database.read() as connection:
            row = self._repository.get_encrypted_profile(connection, user_id)
        if row is None:
            return None
        try:
            payload = json.loads(self._cipher.decrypt(str(row["encrypted_payload"])))
            return BirthProfile(
                birth_date=str(payload["birth_date"]),
                birth_time=str(payload["birth_time"]),
                gender=str(payload["gender"]),
                updated_at=datetime.fromisoformat(str(row["updated_at"])),
            )
        except (ValueError, json.JSONDecodeError, KeyError) as exc:
            raise BusinessError(
                "profile_unavailable", "个人资料暂时无法读取，请联系管理员。"
            ) from exc

    def save_profile(
        self, user_id: int, birth_date: date, birth_time: str, gender: str
    ) -> BirthProfile:
        today = datetime.now(SHANGHAI).date()
        if birth_date > today:
            raise BusinessError("invalid_birth_date", "出生日期不能晚于今天。")
        payload = json.dumps(
            {
                "birth_date": birth_date.isoformat(),
                "birth_time": birth_time,
                "gender": gender,
            },
            ensure_ascii=False,
            separators=(",", ":"),
            sort_keys=True,
        )
        now = now_utc()
        with self._database.transaction() as connection:
            self._repository.save_encrypted_profile(
                connection, user_id, self._cipher.encrypt(payload), now
            )
        return BirthProfile(birth_date.isoformat(), birth_time, gender, now)

    def delete_profile(self, user_id: int) -> None:
        with self._database.transaction() as connection:
            self._repository.delete_profile(connection, user_id)

    def _issue_session(
        self,
        connection: sqlite3.Connection,
        user: UserRecord,
        membership: MembershipRecord,
        now: datetime,
    ) -> SessionIssue:
        token = secrets.token_urlsafe(32)
        csrf_token = secrets.token_urlsafe(32)
        token_hash = hash_token(token)
        csrf_hash = hash_token(csrf_token)
        self._repository.create_session(
            connection,
            token_hash,
            csrf_hash,
            user.id,
            now,
            now + SESSION_LIFETIME,
        )
        return SessionIssue(
            token=token,
            csrf_token=csrf_token,
            principal=Principal(token_hash, csrf_hash, user, membership),
        )


class MembershipService:
    DURATION_MONTHS = {
        "1_month": 1,
        "3_months": 3,
        "12_months": 12,
        "3_years": 36,
    }

    def __init__(
        self, database: Database, repository: AccountRepository, usage: LLMRepository
    ) -> None:
        self._database = database
        self._repository = repository
        self._usage = usage

    def view_for(self, principal: Principal) -> MembershipView:
        today = datetime.now(SHANGHAI).date().isoformat()
        with self._database.read() as connection:
            used_today = self._usage.usage_today(connection, principal.user.id, today)
        return membership_view(principal.user, principal.membership, used_today, now_utc())

    def can_use_smart_features(self, principal: Principal) -> bool:
        return principal.user.is_admin or membership_active(principal.membership, now_utc())

    def list_accounts(self) -> tuple[MembershipAccountView, ...]:
        today = datetime.now(SHANGHAI).date().isoformat()
        now = now_utc()
        with self._database.read() as connection:
            accounts = self._repository.list_memberships(connection)
            usage = self._usage.usage_for_users(connection, today)
        return tuple(
            MembershipAccountView(
                account.user,
                membership_view(
                    account.user,
                    account.membership,
                    usage.get(account.user.id, 0),
                    now,
                ),
            )
            for account in accounts
        )

    def get_account(self, user_id: int) -> MembershipAccountView:
        today = datetime.now(SHANGHAI).date().isoformat()
        with self._database.read() as connection:
            account = self._repository.get_membership_account(connection, user_id)
            used_today = self._usage.usage_today(connection, user_id, today)
        if account is None:
            raise BusinessError("account_not_found", "账号不存在。")
        return MembershipAccountView(
            account.user,
            membership_view(account.user, account.membership, used_today, now_utc()),
        )

    def update(
        self,
        actor: Principal,
        user_id: int,
        action: str,
        duration: str | None,
        daily_limit: int | None,
    ) -> MembershipRecord:
        now = now_utc()
        with self._database.transaction() as connection:
            current = self._repository.get_membership(connection, user_id)
            if current is None:
                raise BusinessError("account_not_found", "账号不存在。")
            limit = daily_limit if daily_limit is not None else current.daily_llm_limit
            if not 1 <= limit <= 1000:
                raise BusinessError("invalid_daily_limit", "每日智能分析上限应为1至1000次。")
            if action == "disable":
                state = "disabled"
                expires_at = current.expires_at
                permanent = current.is_permanent
            elif action == "activate":
                if duration == "permanent":
                    state, expires_at, permanent = "active", None, True
                elif duration in self.DURATION_MONTHS:
                    if current.state == "active" and current.is_permanent:
                        raise BusinessError("permanent_membership", "永久会员无需续期。")
                    base = (
                        current.expires_at
                        if membership_active(current, now) and current.expires_at is not None
                        else now
                    )
                    state = "active"
                    expires_at = add_months(base, self.DURATION_MONTHS[duration])
                    permanent = False
                else:
                    raise BusinessError("invalid_membership_duration", "请选择有效的会员时长。")
            elif action == "set_limit":
                state = current.state
                expires_at = current.expires_at
                permanent = current.is_permanent
            else:
                raise BusinessError("invalid_membership_action", "会员操作无效。")
            updated = self._repository.update_membership(
                connection,
                user_id,
                state,
                expires_at,
                permanent,
                limit,
                actor.user.id,
                now,
            )
            if updated is None:
                raise BusinessError("account_not_found", "账号不存在。")
            return updated
