"""
models.py — все датаклассы бота.
Добавлены методы парсинга прокси и хелперы для инициализации.
"""

import asyncio
import time
import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import Optional, List, Dict, Set, Union


# Ограничение памяти на log-буферы джобов: длинный прогрев/рассылка за недели
# легко накапливает десятки МБ строк в log_lines, что съедает RAM клиента.
_LOG_LINES_CAP = 2000


def log_append(lines: List[str], text: str, cap: int = _LOG_LINES_CAP) -> None:
    """Append с ограничением длины (in-place, чтобы не менять тип поля)."""
    lines.append(text)
    if len(lines) > cap:
        # Отрезаем первые 20% — амортизированное удаление быстрее чем pop(0) на каждый append
        del lines[: len(lines) - cap + cap // 5]


@dataclass
class DeviceInfo:
    device_model: str
    system_version: str
    app_version: str
    lang_code: str
    system_lang_code: str


@dataclass
class ProxyResolved:
    pysocks_tuple: Optional[tuple]  # (proxy_type, addr, port, rdns, user, pass)
    json_vector: Optional[list]

    @staticmethod
    def parse(proxy_str: str) -> Optional["ProxyResolved"]:
        """
        Парсит прокси из строки. Поддерживает форматы:
        - socks5://user:pass@host:port
        - host:port:user:pass
        - host:port (без авторизации)
        """
        if not proxy_str or not isinstance(proxy_str, str):
            return None

        try:
            # Упрощенный парсинг для Telethon (socks5 по умолчанию)
            # proxy_type: 2 = SOCKS5, 1 = SOCKS4, 3 = HTTP
            user, password, host, port = None, None, None, None
            ptype = 2

            if "://" in proxy_str:
                schema, rest = proxy_str.split("://", 1)
                if "http" in schema.lower():
                    ptype = 3
                if "@" in rest:
                    auth, addr = rest.split("@", 1)
                    user, password = auth.split(":", 1)
                    host, port = addr.split(":", 1)
                else:
                    host, port = rest.split(":", 1)
            else:
                parts = proxy_str.split(":")
                if len(parts) == 4:
                    host, port, user, password = parts
                elif len(parts) == 2:
                    host, port = parts

            if host and port:
                p_tuple = (ptype, host, int(port), True, user, password)
                p_vector = [ptype, host, int(port), user, password]
                return ProxyResolved(pysocks_tuple=p_tuple, json_vector=p_vector)
        except Exception:
            return None
        return None


@dataclass
class OutputJSON:
    app_id: int
    app_hash: str
    sdk: str
    device: str
    app_version: str
    lang_pack: str
    system_lang_pack: str
    twoFA: Optional[str]
    role: Optional[str]
    id: int
    phone: str
    username: Optional[str]
    date_of_birth: Optional[int] = None
    date_of_birth_integrity: Optional[str] = None
    is_premium: Optional[bool] = None
    premium_expiry: Optional[str] = None
    first_name: Optional[str] = None
    last_name: Optional[str] = None
    has_profile_pic: Optional[bool] = None
    spamblock: Optional[str] = None
    spamblock_end_date: Optional[str] = None
    session_file: str = ""
    stats_spam_count: int = 0
    stats_invites_count: int = 0
    last_connect_date: str = ""
    session_created_date: str = ""
    app_config_hash: Optional[str] = None
    user_id: int = 0
    device_token: Optional[str] = None
    device_token_secret: Optional[str] = None
    device_secret: Optional[str] = None
    signature: Optional[str] = None
    certificate: Optional[str] = None
    safetynet: Optional[str] = None
    perf_cat: int = 2
    tz_offset: int = 0
    register_time: Optional[int] = None
    last_check_time: Optional[int] = None
    avatar: Optional[str] = "img/default.png"
    sex: Optional[int] = None
    proxy: Optional[list] = None
    ipv6: bool = False
    time: str = ""
    work: int = 0


# Подтверждённо-мёртвые причины. Только эти reasons делают acc.is_confirmed_dead=True.
# Транзиентные ошибки сети/прокси сюда НЕ входят — иначе моргнувший прокси
# превращает живой акк в «мёртвый» во всех фильтрах одновременно.
_CONFIRMED_DEAD_REASONS = {
    "session_revoked", "auth_key_duplicated", "authkeyrevokedsilent",
    "authkeyunregistered", "sessionrevoked", "user_banned", "user_deactivated",
    "manual_move",
}


@dataclass
class AccountStats:
    spam_sent: int = 0
    spam_failed: int = 0
    spam_spamblock: int = 0
    joins_ok: int = 0
    joins_failed: int = 0
    floodwait_count: int = 0
    floodwait_total_sec: int = 0
    last_activity_at: float = 0.0
    created_at: float = field(default_factory=time.time)


@dataclass
class Account:
    phone: str
    username: Optional[str] = None
    session_path: Optional[Path] = None
    json_path: Optional[Path] = None
    client: object = None  # TelegramClient
    dev: Optional[DeviceInfo] = None
    proxy: Optional[ProxyResolved] = None
    known_ids: Set[int] = field(default_factory=set)
    last_join_at: float = 0.0
    warmup: bool = False
    # Флаг «этот акк используется для мониторинга» — выставляется autoload'ом
    # если файлы лежат в папке Аккаунты/Мониторинг/. Дальше autostart мониторинга
    # берёт именно его, а не первый попавшийся из ctx.accounts (раньше после
    # обновы settings.phone иногда сбрасывался и стартовал случайный акк).
    monitor: bool = False
    dead: bool = False
    dead_reason: Optional[str] = None
    # verified — был ли аккаунт хоть раз успешно подключён и валидирован через
    # get_me(). Если False — не показываем «Мёртвый» в UI, даже если dead=True
    # (мог быть транзиентный сбой при autoload). Только после явной проверки
    # ставим финальный статус.
    verified: bool = False
    frozen: bool = False
    frozen_reason: Optional[str] = None
    spam_blocked: bool = False
    spam_block_note: Optional[str] = None
    spam_checked_at: float = 0.0
    # spam_permanent — постоянный спам-блок (Telegram пишет «навсегда»). Раньше
    # хранилось через setattr/getattr и терялось при dataclasses.replace или
    # сериализации — теперь явное поле, апплайтся в apply_state_to_account.
    spam_permanent: bool = False
    # Metadata from JSON
    spamblock_json: Optional[str] = None
    session_created: str = ""
    register_time: int = 0
    folder_name: str = ""
    avatar_cached: Optional[bytes] = None
    floodwait_until: float = 0.0
    floodwait_seconds: int = 0
    quota_paused_until: float = 0.0
    disabled: bool = False
    disabled_reason: Optional[str] = None
    first_name: Optional[str] = None
    last_name: Optional[str] = None
    is_premium: bool = False
    connected_at: float = field(default_factory=time.time)
    stats: AccountStats = field(default_factory=AccountStats)
    # Эконом-режим: аккаунт отключён от Telegram для экономии ресурсов
    idle: bool = False
    idle_since: float = 0.0
    last_used_at: float = field(default_factory=time.time)

    @property
    def is_floodwaiting(self) -> bool:
        return self.floodwait_until > time.time()

    @property
    def floodwait_remaining_sec(self) -> int:
        return max(0, int(self.floodwait_until - time.time()))

    @property
    def floodwait_remaining_str(self) -> str:
        sec = self.floodwait_remaining_sec
        if sec <= 0:
            return "нет"
        if sec < 60:
            return f"{sec}с"
        if sec < 3600:
            return f"{sec // 60}мин"
        h, m = divmod(sec // 60, 60)
        return f"{h}ч {m}мин" if h < 24 else f"{sec // 86400}д"

    @property
    def is_available(self) -> bool:
        return not (self.is_confirmed_dead or self.frozen or self.disabled or self.is_floodwaiting)

    @property
    def is_confirmed_dead(self) -> bool:
        """Только подтверждённо мёртвые. Транзиентные dead-флаги (моргнул прокси,
        не дотянулся init_connection) не считаем мёртвыми — акку дадим шанс
        подключиться при первом действии. Раньше из-за этого юзер видел
        «21 акк живых», но build_warmup_pairs возвращал 1 → «got 1»."""
        if not self.dead:
            return False
        reason = (self.dead_reason or "").lower()
        return reason in _CONFIRMED_DEAD_REASONS

    def status_icon(self) -> str:
        if self.dead:
            return "💀"
        if self.frozen:
            return "❄️"
        if self.disabled:
            return "🚫"
        if self.is_floodwaiting:
            return "⏳"
        if self.spam_blocked:
            return "🔴"
        if self.idle:
            return "💤"
        return "🟢"

    def status_text(self) -> str:
        if self.dead:
            return f"Мертв ({self.dead_reason or '?'})"
        if self.frozen:
            return f"Заморожен ({self.frozen_reason or '?'})"
        if self.disabled:
            return f"Отключен: {self.disabled_reason or '?'}"
        if self.is_floodwaiting:
            return f"FloodWait {self.floodwait_remaining_str}"
        if self.idle:
            idle_min = int((time.time() - self.idle_since) / 60) if self.idle_since else 0
            return f"Сон ({idle_min} мин)"
        return "Активен"

    def touch(self):
        """Обновляет время последнего использования (для эконом-режима)."""
        self.last_used_at = time.time()
        self.stats.last_activity_at = time.time()


@dataclass
class UserStats:
    total_spam_sent: int = 0
    total_spam_failed: int = 0
    total_joins: int = 0
    total_joins_failed: int = 0
    total_floodwaits: int = 0
    tasks_completed: int = 0
    tasks_failed: int = 0
    last_active_at: float = 0.0
    registered_at: float = field(default_factory=time.time)


@dataclass
class JoinJob:
    owner_id: int
    acc: Account
    queue: List[str]
    total: int
    dup_removed: int
    results: Dict[str, str] = field(default_factory=dict)
    processed: int = 0
    counts: Dict[str, int] = field(
        default_factory=lambda: {
            "joined": 0,
            "already": 0,
            "request": 0,
            "ignored": 0,
            "flood": 0,
            "failed": 0,
            "other": 0,
            "captcha_solved": 0,
        }
    )
    started_at: float = field(default_factory=lambda: time.time())
    msg_chat_id: Optional[int] = None
    msg_id: Optional[int] = None
    status: str = "idle"
    pause_event: asyncio.Event = field(default_factory=lambda: asyncio.Event())
    stop_flag: bool = False
    task: Optional[asyncio.Task] = None
    job_id: Optional[str] = None
    current_link: Optional[str] = None
    current_step: str = ""
    last_result: Optional[str] = None
    last_event: str = ""
    ui_last_update_at: float = 0.0
    log_lines: List[str] = field(default_factory=list)
    # Лимит на акк: после скольких УСПЕШНЫХ подписок (joined / already / request)
    # акк сам останавливается. 0 = без лимита (старое поведение — до конца списка
    # или до квоты Telegram). Считаются: joined + already_member + request_sent.
    # FloodWait-промахи и failed НЕ учитываются — они и так не «занимают слот».
    max_per_acc: int = 0


@dataclass
class SpamJob:
    owner_id: int
    acc: Account
    text: str
    targets: list                  # List[int] для чатов, List[str] для DM (@username)
    titles: dict                   # Dict[int/str, str]
    total: int
    processed: int = 0
    results: Dict[int, str] = field(default_factory=dict)
    counts: Dict[str, int] = field(
        default_factory=lambda: {
            "sent": 0,
            "spamfilter": 0,
            "flood": 0,
            "slowmode": 0,
            "no_rights": 0,
            "failed": 0,
        }
    )
    started_at: float = field(default_factory=lambda: time.time())
    msg_chat_id: Optional[int] = None
    msg_id: Optional[int] = None
    status: str = "idle"
    pause_event: asyncio.Event = field(default_factory=lambda: asyncio.Event())
    stop_flag: bool = False
    task: Optional[asyncio.Task] = None
    job_id: Optional[str] = None
    current_step: str = ""
    last_result: Optional[str] = None
    ui_last_update_at: float = 0.0
    log_lines: List[str] = field(default_factory=list)
    image_path: Optional[str] = None  # путь к картинке для рассылки
    dm_mode: bool = False              # True = рассылка по юзерам (DM)
    # Лимит на акк: сколько ЛС/сообщений может отправить ОДИН аккаунт за задачу.
    # 0 = без лимита (работает до спамблока, как раньше). Иначе job останавливается
    # для этого акка после max_per_acc успешных отправок. В мульти-режиме
    # каждый акк считает свой лимит отдельно.
    max_per_acc: int = 0


@dataclass
class WarmupPair:
    phone_a: str
    phone_b: str
    conversation_id: str = ""
    topic: str = ""
    messages_exchanged: int = 0
    last_message_at: float = 0.0
    last_sender: str = ""
    state: str = "idle"          # idle → greeting → chatting → goodbye → done
    entity_a_id: int = 0         # cached Telegram user ID
    entity_b_id: int = 0


@dataclass
class WarmupSchedule:
    total_days: int = 21
    current_day: int = 1
    started_at: float = 0.0
    stage: str = "light"         # light → warmup → active → boost → stable → done
    max_peers_today: int = 5
    msgs_per_day: int = 8
    auto_move_when_done: bool = True
    # Override авто-расчёта: если >0 — используется как msgs_per_day весь
    # цикл независимо от дня и числа аккаунтов. 0 = классический авто-режим.
    msgs_per_day_override: int = 0


@dataclass
class WarmupJob:
    owner_id: int
    pairs: List[WarmupPair] = field(default_factory=list)
    schedule: WarmupSchedule = field(default_factory=WarmupSchedule)
    status: str = "idle"         # idle / running / paused / stopped / finished
    pause_event: asyncio.Event = field(default_factory=lambda: asyncio.Event())
    stop_flag: bool = False
    started_at: float = field(default_factory=time.time)
    total_messages_sent: int = 0
    messages_today: int = 0
    last_day_reset: float = 0.0
    log_lines: List[str] = field(default_factory=list)
    task: Optional[asyncio.Task] = None
    job_id: Optional[str] = None
    config: dict = field(default_factory=lambda: {
        "active_hours_start": 9,
        "active_hours_end": 22,
        "min_delay": 60,
        "max_delay": 300,
        "burst_size": 3,
        "burst_pause_min": 600,
        "burst_pause_max": 1800,
        "gen_mode": "auto",       # auto / ai / templates
        # Сценарии прогрева
        "scenarios": {
            "dm": True,            # переписка между своими аккаунтами
            "channels": False,     # подписка на популярные каналы
            "reactions": False,    # реакции в каналах
            "chats": False,        # подписка на чаты + 1-2 сообщения
        },
        "channels_list": [],       # юзернеймы каналов (без @)
        "chats_list": [],          # инвайт-ссылки или юзернеймы чатов
        "channels_per_day": 3,     # подписок на акк в день
        "chat_messages_per_day": 2,
        "reactions_per_day": 5,
        "reply_to_incoming": True, # читать последнее входящее и отвечать контекстно
    })
    # Состояние по аккаунту: {phone: {channels_joined: [...], chats_joined: [...], reactions_today: int, chat_msgs_today: int, subs_today: int}}
    account_state: dict = field(default_factory=dict)


@dataclass
class CheckerJob:
    """Чекер аккаунтов: проверка статуса + опц. снятие спамблока/заморозки."""
    config: dict = field(default_factory=dict)
    target_phones: List[str] = field(default_factory=list)
    status: str = "running"  # running / finished / stopped
    total: int = 0
    done: int = 0
    alive_count: int = 0
    temp_spam_count: int = 0
    eternal_spam_count: int = 0
    frozen_count: int = 0
    error_count: int = 0
    log_lines: List[str] = field(default_factory=list)
    started_at: float = 0.0


@dataclass
class BoostJob:
    """Накрутка: подписчики/реакции/опросы/кнопки/боты/просмотры."""
    config: object = None  # BoostConfig (из core/boost.py — чтобы избежать circular import)
    status: str = "running"  # running / finished / stopped
    total: int = 0
    done: int = 0
    ok: int = 0
    fail: int = 0
    log_lines: List[str] = field(default_factory=list)
    started_at: float = 0.0


@dataclass
class OwnerCtx:
    accounts: Dict[str, Account] = field(default_factory=dict)
    active: Optional[str] = None
    mode: Optional[str] = None
    temp: dict = field(default_factory=dict)
    job: Optional[JoinJob] = None
    spam: dict = field(
        default_factory=lambda: {
            "template": None,
            "targets": [],
            "titles": {},
            "writable_count": 0,
            "image_path": None,
        }
    )
    spam_job: Optional[SpamJob] = None
    user_spam: dict = field(
        default_factory=lambda: {
            "template": None,
            "targets": [],
            "titles": {},
            "image_path": None,
        }
    )
    user_spam_job: Optional[SpamJob] = None
    warmup_job: Optional[WarmupJob] = None
    boost_job: Optional[BoostJob] = None
    checker_job: Optional[CheckerJob] = None
    stats: UserStats = field(default_factory=UserStats)
    user_settings: dict = field(
        default_factory=lambda: {
            "join_min_interval": 18,
            "join_jitter": 3.0,
            "join_max_per_session": 10,
            "join_session_break_min": 180,
            "join_session_break_max": 360,
            "spam_min_delay": 6.0,
            "spam_max_delay": 14.0,
            "spam_burst_size": 7,
            "spam_burst_pause_min": 120,
            "multi_max_accounts": 10,
            "dm_min_delay": 35,
            "dm_max_delay": 90,
            "dm_burst_size": 3,
            "dm_burst_pause_min": 300,
            "dm_burst_pause_max": 600,
            "loop_mode": False,
            "loop_interval_min": 5,
            "spam_rounds": 1,
        }
    )
