﻿"""config.json'daki ayarlarÄ± kullanarak TLS tabanlÄ± iÅŸÃ§i thread'lerini yÃ¶neten giriÅŸ noktasÄ±."""
from __future__ import annotations

import itertools
import logging
import os
import re
import signal
import threading
import time
from pathlib import Path
from typing import Any, Callable, Dict, Iterable, Iterator, List, Optional

from anymessage import (
    fetch_balance as anymessage_fetch_balance,
    get_available_domain_count_with_retry,
)
from balance_monitor import BalanceMonitor, DEFAULT_BALANCE_THRESHOLD
from email_utils import is_gmail_domain_alias
from captcha_service import get_balance
from castle_solver import CastleSolverError, fetch_balance as castle_fetch_balance
from disposablemail import (
    DisposableMailAPI,
    DisposableMailAPIError,
    resolve_mail_service,
)
from mail_stats import get_mail_stats
from metrics_tracker import start_metrics_display, stop_metrics_display
from xgen import (
    ANYMESSAGE_POLICY_AUTO,
    ANYMESSAGE_POLICY_KEY,
    ANYMESSAGE_POLICY_PURCHASE_ONLY,
    ANYMESSAGE_POLICY_REUSE_ONLY,
    ANYMESSAGE_STOCK_SITE,
    ANYMESSAGE_STOCK_THRESHOLD,
    DEBUG_LOG_PATH,
    create_worker_thread,
    get_global_shutdown_reason,
    request_global_shutdown,
    request_global_resource_wait,
    is_debug_logging_enabled,
    load_config,
    is_global_shutdown_requested,
    start_worker_threads,
    has_available_rereg_email,
    count_available_rereg_email,
    clear_global_resource_wait,
    is_global_resource_wait_requested,
)


CONFIG_PATH = Path(__file__).with_name("config.json")
LOGGER = logging.getLogger(__name__)
_STARTUP_LOCK = threading.Lock()
_STARTUP_DONE = False
_CACHED_CONFIG: Optional[Dict[str, Any]] = None
_ANYMESSAGE_STOCK_CHECK_INTERVAL = 3.0
_ANYMESSAGE_STOCK_RETRY_ATTEMPTS = 5
_ANYMESSAGE_STOCK_RETRY_DELAY = 1.5
_THREAD_SCALE_CHECK_INTERVAL = 5.0


def _is_feature_enabled(value: Any) -> bool:
    if isinstance(value, bool):
        return value
    if isinstance(value, (int, float)):
        return value != 0
    if isinstance(value, str):
        lowered = value.strip().lower()
        return lowered in {"1", "true", "yes", "on"}
    return False


def _is_manual_email_mode_enabled(config: Dict[str, Any]) -> bool:
    if not config.get("manuel-email"):
        return False
    return bool(is_debug_logging_enabled(config))


def _should_apply_anymessage_policy(config: Dict[str, Any]) -> bool:
    if _is_manual_email_mode_enabled(config):
        return False

    if not _is_feature_enabled(config.get("anymessage")):
        return False

    mail_service = str(config.get("mail_service") or "").strip().lower()
    if mail_service and mail_service != "anymessage":
        return False

    domain = str(config.get("anymessage_domain") or "").strip().lower()
    if not domain:
        domain = "gmail.com"
    return is_gmail_domain_alias(domain)


def _query_anymessage_stock_count(api_key: str, domain: str) -> Optional[int]:
    if not api_key:
        return None

    return get_available_domain_count_with_retry(
        api_key,
        domain=domain or "gmail.com",
        site=ANYMESSAGE_STOCK_SITE,
        attempts=_ANYMESSAGE_STOCK_RETRY_ATTEMPTS,
        retry_delay=_ANYMESSAGE_STOCK_RETRY_DELAY,
        logger=LOGGER,
    )


def _wait_for_anymessage_resources(config: Dict[str, Any]) -> str:
    previous_policy = str(
        config.get(ANYMESSAGE_POLICY_KEY) or ANYMESSAGE_POLICY_AUTO
    ).strip().lower()

    if not _should_apply_anymessage_policy(config):
        config[ANYMESSAGE_POLICY_KEY] = ANYMESSAGE_POLICY_AUTO
        if is_global_resource_wait_requested():
            clear_global_resource_wait()
        return ANYMESSAGE_POLICY_AUTO

    api_key = str(config.get("AnyMessage") or "").strip()
    if not api_key:
        LOGGER.warning(
            "AnyMessage Gmail stock policy disabled because the API key is missing."
        )
        config[ANYMESSAGE_POLICY_KEY] = ANYMESSAGE_POLICY_AUTO
        return ANYMESSAGE_POLICY_AUTO

    domain = str(config.get("anymessage_domain") or "").strip() or "gmail.com"
    interval = _ANYMESSAGE_STOCK_CHECK_INTERVAL
    prefer_purchase = _is_feature_enabled(
        config.get("anymessage_prioritize_purchase", True)
    )

    while True:
        stock_count = _query_anymessage_stock_count(api_key, domain)
        has_rereg_email = has_available_rereg_email(config)
        has_reuse_email = has_rereg_email
        stock_ready = (
            stock_count is not None
            and stock_count >= ANYMESSAGE_STOCK_THRESHOLD
        )

        reuse_sources = []
        if has_rereg_email:
            reuse_sources.append("Email_information_awaiting_re-registration.txt")
        reuse_text = " and ".join(reuse_sources) if reuse_sources else "re-registration lists"

        if prefer_purchase:
            if stock_ready:
                clear_global_resource_wait()
                config[ANYMESSAGE_POLICY_KEY] = ANYMESSAGE_POLICY_PURCHASE_ONLY
                if previous_policy != ANYMESSAGE_POLICY_PURCHASE_ONLY:
                    LOGGER.info(
                        "AnyMessage Gmail stock is sufficient (count=%d); mail orders will continue in purchase-only mode.",
                        stock_count,
                    )
                return ANYMESSAGE_POLICY_PURCHASE_ONLY

            if has_reuse_email:
                clear_global_resource_wait()
                config[ANYMESSAGE_POLICY_KEY] = ANYMESSAGE_POLICY_REUSE_ONLY
                if previous_policy != ANYMESSAGE_POLICY_REUSE_ONLY:
                    detail = "bilinmiyor" if stock_count is None else str(stock_count)
                    LOGGER.warning(
                        "AnyMessage Gmail stock (%s) is insufficient; %s will be used.",
                        detail,
                        reuse_text,
                    )
                return ANYMESSAGE_POLICY_REUSE_ONLY
        else:
            if has_reuse_email:
                clear_global_resource_wait()
                config[ANYMESSAGE_POLICY_KEY] = ANYMESSAGE_POLICY_REUSE_ONLY
                if previous_policy != ANYMESSAGE_POLICY_REUSE_ONLY:
                    LOGGER.info(
                        "AnyMessage Gmail priority set to re-registration lists; %s will be used.",
                        reuse_text,
                    )
                return ANYMESSAGE_POLICY_REUSE_ONLY

            if stock_ready:
                clear_global_resource_wait()
                config[ANYMESSAGE_POLICY_KEY] = ANYMESSAGE_POLICY_PURCHASE_ONLY
                if previous_policy != ANYMESSAGE_POLICY_PURCHASE_ONLY:
                    detail = "bilinmiyor" if stock_count is None else str(stock_count)
                    LOGGER.warning(
                        "Re-registration lists are preferred but empty; AnyMessage stock (%s) is sufficient, switching to purchase mode.",
                        detail,
                    )
                return ANYMESSAGE_POLICY_PURCHASE_ONLY

        stock_text = "bilinmiyor"
        if stock_count is not None:
            stock_text = str(stock_count)
        if not is_global_resource_wait_requested():
            request_global_resource_wait(
                "AnyMessage Gmail stock is low and the re-registration list is empty."
            )
        LOGGER.warning(
            "AnyMessage Gmail stock (%s) is low and no reusable email is available in the re-registration list. Waiting %.1f seconds.",
            stock_text,
            interval,
        )
        time.sleep(interval)


def _worker_id_from_thread(thread: threading.Thread) -> Optional[int]:
    """Thread ismine gÃ¶re worker ID deÄŸerini Ã§Ä±kar."""

    name = thread.name or ""
    match = re.fullmatch(r"Thread(\d+)", name)
    if match:
        try:
            return int(match.group(1))
        except ValueError:
            return None
    return None


def _reserve_worker_id(
    active_ids: Iterable[Optional[int]],
    configured_thread_count: int,
) -> Optional[int]:
    used = {
        value
        for value in active_ids
        if isinstance(value, int) and value > 0
    }
    for candidate in range(1, max(1, configured_thread_count) + 1):
        if candidate not in used:
            return candidate
    return None


def _supports_color(stream: Any) -> bool:
    """Stream nesnesinin ANSI renk kodlarÄ±nÄ± destekleyip desteklemediÄŸini belirt."""

    try:
        is_a_tty = bool(stream.isatty())
    except Exception:  # pragma: no cover - platform baÄŸÄ±mlÄ±
        is_a_tty = False

    if not is_a_tty:
        return False

    term = os.environ.get("TERM", "")
    return term.lower() != "dumb"


class _ColorFormatter(logging.Formatter):
    """Log seviyelerine gÃ¶re mesajlarÄ± renklendiren formatter."""

    RESET = "\033[0m"
    LEVEL_COLORS = {
        logging.DEBUG: "\033[36m",  # DiÄŸer detay loglarÄ±: camgÃ¶beÄŸi
        logging.INFO: "\033[32m",  # BaÅŸarÄ±lÄ± yanÄ±tlar: yeÅŸil
        logging.WARNING: "\033[33m",  # UyarÄ±lar: sarÄ±
        logging.ERROR: "\033[31m",  # BaÅŸarÄ±sÄ±z yanÄ±tlar: kÄ±rmÄ±zÄ±
        logging.CRITICAL: "\033[35m",  # Kritik durumlar: mor
    }

    THREAD_COLORS = [
        "\033[38;5;39m",   # vivid blue
        "\033[38;5;208m",  # orange
        "\033[38;5;213m",  # pink
        "\033[38;5;112m",  # green
        "\033[38;5;81m",   # aqua
        "\033[38;5;190m",  # yellow-green
        "\033[38;5;177m",  # magenta
        "\033[38;5;69m",   # teal
    ]
    DEFAULT_THREAD_COLOR = "\033[38;5;244m"
    COUNTRY_CODE_COLOR = "\033[38;5;45m"
    ANSI_ESCAPE_RE = re.compile(r"\033\[[0-9;]*m")
    STATUS_CODE_RE = re.compile(
        r"(?P<prefix>\b(?:status(?:_code)?|response)\s*[:=]\s*)(?P<code>\d{3})",
        re.IGNORECASE,
    )
    COUNTRY_CODE_RE = re.compile(
        r"(Country code:\s*)(?P<code>[A-Z]{2})",
        re.IGNORECASE,
    )
    SIGNUP_DENIED_RE = re.compile(r"signup denied", re.IGNORECASE)

    def __init__(self, fmt: str, *, colorize: bool) -> None:
        super().__init__(fmt)
        self._colorize = colorize
        self._thread_colors: Dict[str, str] = {}

    def _get_thread_color(self, thread_name: str) -> str:
        if not thread_name:
            return self.DEFAULT_THREAD_COLOR

        cached = self._thread_colors.get(thread_name)
        if cached:
            return cached

        index = len(self._thread_colors) % len(self.THREAD_COLORS)
        color = self.THREAD_COLORS[index]
        self._thread_colors[thread_name] = color
        return color

    def _apply_highlights(self, message: str) -> str:
        def _status_replacer(match: re.Match[str]) -> str:
            code_text = match.group("code")
            try:
                code_value = int(code_text)
            except ValueError:
                code_value = None

            if code_value is None:
                status_color = self.LEVEL_COLORS.get(logging.WARNING, "")
            elif code_value >= 500:
                status_color = self.LEVEL_COLORS.get(logging.ERROR, "")
            elif code_value >= 400:
                status_color = self.LEVEL_COLORS.get(logging.ERROR, "")
            elif code_value >= 300:
                status_color = self.LEVEL_COLORS.get(logging.WARNING, "")
            elif code_value >= 200:
                status_color = self.LEVEL_COLORS.get(logging.INFO, "")
            else:
                status_color = self.LEVEL_COLORS.get(logging.DEBUG, "")

            return f"{match.group('prefix')}{status_color}{code_text}{self.RESET}"

        def _country_replacer(match: re.Match[str]) -> str:
            return (
                f"{match.group(1)}"
                f"{self.COUNTRY_CODE_COLOR}{match.group('code').upper()}{self.RESET}"
            )

        def _signup_denied_replacer(match: re.Match[str]) -> str:
            error_color = self.LEVEL_COLORS.get(logging.ERROR, "")
            return f"{error_color}{match.group(0)}{self.RESET}"

        highlighted = self.STATUS_CODE_RE.sub(_status_replacer, message)
        highlighted = self.COUNTRY_CODE_RE.sub(_country_replacer, highlighted)
        highlighted = self.SIGNUP_DENIED_RE.sub(_signup_denied_replacer, highlighted)
        return highlighted

    def _pick_message_color(
        self, record: logging.LogRecord, default_color: str
    ) -> str:
        message = record.message or ""
        status_match = self.STATUS_CODE_RE.search(message)
        if status_match:
            try:
                status_code = int(status_match.group("code"))
            except ValueError:
                status_code = None
            if status_code is not None:
                if status_code >= 500:
                    return self.LEVEL_COLORS.get(logging.ERROR, default_color)
                if status_code >= 400:
                    return self.LEVEL_COLORS.get(logging.ERROR, default_color)
                if status_code >= 300:
                    return self.LEVEL_COLORS.get(logging.WARNING, default_color)
                if status_code >= 200:
                    return default_color
        lowered = message.lower()
        if any(keyword in lowered for keyword in ("failed", "timeout", "denied")):
            return self.LEVEL_COLORS.get(logging.ERROR, default_color)
        return default_color

    def _style_message(self, record: logging.LogRecord, level_color: str) -> str:
        if not self._colorize:
            return record.message

        base_color = "" if record.levelno == logging.INFO else level_color
        message_color = self._pick_message_color(record, base_color)
        styled = self._apply_highlights(record.message)
        restore_color = f"{self.RESET}{message_color}" if message_color else self.RESET
        styled = styled.replace(self.RESET, restore_color)
        return f"{message_color}{styled}{self.RESET}"

    def format(self, record: logging.LogRecord) -> str:
        record.message = record.getMessage()

        if record.exc_info:
            if not record.exc_text:
                record.exc_text = self.formatException(record.exc_info)
        else:
            record.exc_text = None

        stack_output = ""
        if record.stack_info:
            stack_output = self.formatStack(record.stack_info)

        base_message = f"{record.levelname} [{record.threadName}]: {record.message}"
        if record.exc_text:
            base_message = f"{base_message}\n{record.exc_text}"
        if stack_output:
            base_message = f"{base_message}\n{stack_output}"

        if not self._colorize:
            return base_message

        level_color = self.LEVEL_COLORS.get(record.levelno, "\033[36m")
        thread_color = self._get_thread_color(record.threadName)
        message_text = self._style_message(record, level_color)

        colored = (
            f"{level_color}{record.levelname}{self.RESET} "
            f"[{thread_color}{record.threadName}{self.RESET}]: "
            f"{message_text}"
        )

        if record.exc_text:
            colored = f"{colored}\n{level_color}{record.exc_text}{self.RESET}"
        if stack_output:
            colored = f"{colored}\n{stack_output}"

        return colored


def _configure_logging() -> None:
    """Renkli log Ã§Ä±ktÄ±sÄ± iÃ§in kÃ¶k logger'Ä± yapÄ±landÄ±r."""

    handler = logging.StreamHandler()
    formatter = _ColorFormatter(
        "%(levelname)s [%(threadName)s]: %(message)s",
        colorize=_supports_color(handler.stream),
    )
    handler.setFormatter(formatter)

    root_logger = logging.getLogger()
    root_logger.handlers.clear()
    root_logger.addHandler(handler)
    root_logger.setLevel(logging.INFO)


def _clear_debug_log() -> None:
    try:
        DEBUG_LOG_PATH.parent.mkdir(parents=True, exist_ok=True)
        DEBUG_LOG_PATH.write_text("")
    except OSError as exc:
        LOGGER.warning("Failed to clear log.txt: %s", exc)


def _wait_for_threads(
    config: Dict[str, Any],
    threads: List[threading.Thread],
    worker_id_counter: Iterator[int],
    *,
    configured_thread_count: int,
    allow_restart: bool = True,
    scale_check_interval: float = _THREAD_SCALE_CHECK_INTERVAL,
) -> bool:
    """Thread'lerin tamamlanmas?n? bekle ve kesmeleri yakala."""

    interrupted = False
    sigint_received = threading.Event()

    def _handle_sigint(signum: int, frame: Any) -> None:
        sigint_received.set()

    previous_handler = signal.getsignal(signal.SIGINT)
    signal.signal(signal.SIGINT, _handle_sigint)
    active_threads = list(threads)
    worker_ids = [
        _worker_id_from_thread(thread)
        for thread in active_threads
    ]
    restart_enabled = allow_restart
    shutdown_wait_logged = False
    target_thread_count = len(active_threads)
    last_scale_check = time.monotonic()
    scale_check_interval = max(0.5, float(scale_check_interval))
    try:
        while active_threads:
            for index in range(len(active_threads) - 1, -1, -1):
                thread = active_threads[index]
                thread.join(timeout=0.2)
                if sigint_received.is_set():
                    interrupted = True
                    restart_enabled = False
                if is_global_shutdown_requested():
                    restart_enabled = False
                    if not shutdown_wait_logged:
                        LOGGER.info(
                            "Global shutdown requested; waiting for worker threads to exit safely."
                        )
                        shutdown_wait_logged = True
                if thread.is_alive():
                    continue

                active_threads.pop(index)
                worker_id = worker_ids.pop(index)
                if restart_enabled and len(active_threads) < target_thread_count:
                    if worker_id is None:
                        worker_id = _reserve_worker_id(
                            worker_ids, configured_thread_count
                        )
                    if worker_id is None:
                        LOGGER.warning(
                            "Thread '%s' has no worker ID; could not restart.",
                            thread.name,
                        )
                        continue
                    _wait_for_anymessage_resources(config)
                    replacement = create_worker_thread(config, worker_id)
                    active_threads.insert(index, replacement)
                    worker_ids.insert(index, _worker_id_from_thread(replacement))

            now = time.monotonic()
            if (
                restart_enabled
                and not is_global_shutdown_requested()
                and now - last_scale_check >= scale_check_interval
            ):
                last_scale_check = now
                try:
                    desired_threads = _estimate_email_slot_limit(
                        config, configured_thread_count
                    )
                except Exception:
                    LOGGER.exception(
                        "Thread scaling check failed; keeping current target (%d).",
                        target_thread_count,
                    )
                    desired_threads = target_thread_count

                if desired_threads != target_thread_count:
                    LOGGER.info(
                        "Adjusting worker thread target based on email resources: %d -> %d.",
                        target_thread_count,
                        desired_threads,
                    )
                    target_thread_count = desired_threads

                while (
                    restart_enabled
                    and len(active_threads) < target_thread_count
                    and not is_global_shutdown_requested()
                ):
                    worker_id = _reserve_worker_id(
                        worker_ids, configured_thread_count
                    )
                    if worker_id is None:
                        LOGGER.warning(
                            "Cannot start additional worker; no free worker ID within configured limit."
                        )
                        break
                    _wait_for_anymessage_resources(config)
                    new_thread = create_worker_thread(config, worker_id)
                    active_threads.append(new_thread)
                    worker_ids.append(_worker_id_from_thread(new_thread))
    finally:
        signal.signal(signal.SIGINT, previous_handler)

    return interrupted

def _resolve_thread_count(config: Dict[str, Any]) -> int:
    raw_value = config.get("Thread") or config.get("thread_count")
    try:
        thread_count = int(raw_value)
    except (TypeError, ValueError):
        LOGGER.warning(
            "Invalid thread count value (%s); defaulting to 1.",
            raw_value,
        )
        return 1

    if thread_count <= 0:
        LOGGER.warning("Thread count was %s; forcing to 1.", thread_count)
        return 1

    return thread_count


def _estimate_email_slot_limit(config: Dict[str, Any], configured_threads: int) -> int:
    """
    Email kaynaklarına göre aynı anda çalışabilecek makul işçi sayısını sınırla.
    Kaynak bilgisi alınamazsa config'deki thread sayısını kullanır.
    """

    max_threads = max(1, int(configured_threads))
    mail_service = str(config.get("mail_service") or "").strip().lower()
    if mail_service and mail_service != "anymessage":
        return max_threads

    # Reuse listelerindeki hazır kayıtlar
    reuse_ready = count_available_rereg_email(config)
    ready_from_lists = reuse_ready

    # AnyMessage stok sayısı (sadece sorgulanabiliyorsa)
    stock_count: Optional[int] = None
    try:
        if _should_apply_anymessage_policy(config):
            api_key = str(config.get("AnyMessage") or "").strip()
            domain = str(config.get("anymessage_domain") or "").strip() or "gmail.com"
            stock_count = _query_anymessage_stock_count(api_key, domain)
    except Exception:
        LOGGER.exception("Could not query AnyMessage stock count.")
        stock_count = None

    available_from_stock = 0
    if isinstance(stock_count, int) and stock_count >= 0:
        available_from_stock = stock_count

    total_available = ready_from_lists + available_from_stock

    # Hiç veri yoksa (stok sorgusu başarısız) config değerini koru
    if stock_count is None and ready_from_lists == 0:
        return max_threads

    # Kaynak sayısı biliniyor; sınırla
    if total_available <= 0:
        return 0

    return max(1, min(max_threads, total_available))


def _load_config_once() -> Dict[str, Any]:
    global _CACHED_CONFIG
    with _STARTUP_LOCK:
        if _CACHED_CONFIG is None:
            LOGGER.info("Main program started.")
            LOGGER.info("Loading config from %s.", CONFIG_PATH)
            _CACHED_CONFIG = load_config(CONFIG_PATH)
        return dict(_CACHED_CONFIG)


def _run_startup_checks(config: Dict[str, Any]) -> None:
    global _STARTUP_DONE
    with _STARTUP_LOCK:
        if _STARTUP_DONE:
            return

        castle_config = config.get("castlesolver.cc")
        castle_api_key = ""
        castle_timeout = 30
        if isinstance(castle_config, dict) and _is_feature_enabled(
            castle_config.get("enabled", True)
        ):
            castle_api_key = str(castle_config.get("api_key") or "").strip()
            timeout_value = castle_config.get("balance_timeout") or castle_config.get(
                "timeout"
            )
            try:
                castle_timeout = int(timeout_value or 30)
            except (TypeError, ValueError):
                castle_timeout = 30

        api_key = str(config.get("code_solver_api_key") or "").strip()
        if not api_key and not castle_api_key:
            raise ValueError(
                "config.json iÃ§inde 'code_solver_api_key' veya 'castlesolver.cc.api_key' anahtarÄ±ndan en az biri bulunmalÄ±."
            )

        castle_exception: Optional[Exception] = None
        castle_response_obtained = False
        code_solver_response_obtained = False

        def _evaluate_balance_response(
            service_name: str,
            response: Any,
            parser: Callable[[Dict[str, Any]], Optional[float]],
        ) -> None:
            LOGGER.info("%s response: %s", service_name, response)
            if isinstance(response, dict):
                solver_balance = parser(response)
                if solver_balance is not None:
                    LOGGER.info(
                        "%s balance initially found as %.4f USD.",
                        service_name,
                        solver_balance,
                    )
                    if solver_balance < DEFAULT_BALANCE_THRESHOLD:
                        message = (
                            f"{service_name} balance is critical ({solver_balance:.4f} USD). "
                            "Application will shut down safely."
                        )
                        LOGGER.error(message)
                        request_global_shutdown(message)
                else:
                    LOGGER.warning(
                        "Could not read balance value from %s response: %s",
                        service_name,
                        response,
                    )
            else:
                LOGGER.warning(
                    "%s balance returned unexpected format: %s",
                    service_name,
                    response,
                )

        if castle_api_key:
            LOGGER.info("Querying CastleSolver balance...")
            try:
                castle_response = castle_fetch_balance(
                    castle_api_key,
                    timeout=castle_timeout,
                )
                if castle_response is not None:
                    castle_response_obtained = True
                    _evaluate_balance_response(
                        "CastleSolver",
                        castle_response,
                        BalanceMonitor.parse_castle_solver_balance,
                    )
            except CastleSolverError as exc:
                LOGGER.warning("Failed to query CastleSolver balance: %s", exc)
                castle_exception = exc
            except Exception as exc:  # pragma: no cover - sadece loglama amaÃ§lÄ±
                LOGGER.exception("Unexpected error while querying CastleSolver balance.")
                castle_exception = exc

        if api_key:
            LOGGER.info("Querying code solver balance...")
            try:
                response = get_balance(api_key)
            except Exception:  # pragma: no cover - sadece loglama amaÃ§lÄ±
                LOGGER.exception("Error while querying code solver balance.")
                if not castle_response_obtained:
                    raise
            else:
                if response is not None:
                    code_solver_response_obtained = True
                    _evaluate_balance_response(
                        "Code solver",
                        response,
                        BalanceMonitor.parse_code_solver_balance,
                    )

        if not castle_response_obtained and not code_solver_response_obtained:
            if not api_key:
                if castle_exception:
                    raise castle_exception
                raise ValueError(
                    "CastleSolver bakiyesi sorgulanamadÄ± ve 'code_solver_api_key' anahtarÄ± yapÄ±landÄ±rÄ±lmamÄ±ÅŸ."
                )
            raise RuntimeError("Code solver bakiyesi sorgusu baÅŸarÄ±lÄ± yanÄ±t dÃ¶ndÃ¼rmedi.")

        manual_email_enabled = bool(config.get("manuel-email")) and is_debug_logging_enabled(
            config
        )

        if manual_email_enabled:
            config["mail_service"] = "manual"
            LOGGER.info(
                "Manual email mode enabled; mail service balance check will be skipped."
            )
        else:
            LOGGER.info("Selecting mail service...")
            try:
                mail_service_name = resolve_mail_service(config)
            except Exception:
                LOGGER.exception("Mail service selection failed.")
                raise

            config["mail_service"] = mail_service_name
            LOGGER.info("Querying mail service balance (%s)...", mail_service_name)

            try:
                if mail_service_name == "disposablemail":
                    mail_api = DisposableMailAPI(config)
                    mail_response = mail_api.get_balance()
                else:
                    mail_response = anymessage_fetch_balance()
            except DisposableMailAPIError:  # pragma: no cover - sadece loglama amaÃ§lÄ±
                LOGGER.exception(
                    "Error while querying Online Disposable Mail balance."
                )
                raise
            except Exception:  # pragma: no cover - sadece loglama amaÃ§lÄ±
                LOGGER.exception("Error while querying mail service balance.")
                raise

            if isinstance(mail_response, dict):
                if mail_service_name == "disposablemail":
                    code_value = mail_response.get("code")
                    if code_value == 200:
                        balance_value = mail_response.get("data", "0")
                        LOGGER.info(
                            "Mail service (%s) balance: %s credits",
                            mail_service_name,
                            balance_value,
                        )
                    else:
                        LOGGER.warning("Mail service error: %s", mail_response)
                else:
                    status_value = str(
                        mail_response.get("status")
                        or mail_response.get("Status")
                        or ""
                    ).upper()
                    if status_value == "OK" or status_value == "SUCCESS":
                        balance_value = mail_response.get("balance", "0")
                        LOGGER.info(
                            "Mail service (%s) balance: %s credits",
                            mail_service_name,
                            balance_value,
                        )
                        parsed_balance = BalanceMonitor.parse_mail_balance(mail_service_name, mail_response)
                        if parsed_balance is not None and parsed_balance < DEFAULT_BALANCE_THRESHOLD:
                            message = (
                                f"Mail service ({mail_service_name}) balance is critical "
                                f"({parsed_balance:.4f}). Application will shut down safely."
                            )
                            LOGGER.error(message)
                            request_global_shutdown(message)
                    else:
                        LOGGER.warning("Mail service error: %s", mail_response)
            else:
                LOGGER.warning("Mail service returned unexpected response: %s", mail_response)

        _STARTUP_DONE = True


def main() -> None:
    """Code solver ve captcha sÃ¼reÃ§lerini baÅŸlat."""
    _configure_logging()

    config = _load_config_once()

    if is_debug_logging_enabled(config):
        _clear_debug_log()

    _run_startup_checks(config)

    shutdown_requested = False
    monitor: Optional[BalanceMonitor] = None
    threads: List[threading.Thread] = []
    worker_id_counter: Optional[Iterator[int]] = None

    try:
        if is_global_shutdown_requested():
            reason = get_global_shutdown_reason() or "startup checks"
            LOGGER.error(
                "Startup checks triggered a global shutdown request (%s); worker threads will not start.",
                reason,
            )
            return

        monitor = BalanceMonitor(config, threshold=DEFAULT_BALANCE_THRESHOLD)
        monitor.start()

        # Show live metrics in the terminal title
        start_metrics_display(interval=2.0)

        configured_thread_count = _resolve_thread_count(config)
        restart_on_completion = not is_debug_logging_enabled(config)
        iteration = 0

        try:
            while True:
                iteration += 1
                effective_thread_count = _estimate_email_slot_limit(
                    config, configured_thread_count
                )
                if effective_thread_count <= 0:
                    LOGGER.warning(
                        "No email resources available (reuse+stock=0); waiting before starting workers."
                    )
                    _wait_for_anymessage_resources(config)
                    continue
                if iteration == 1:
                    if effective_thread_count < configured_thread_count:
                        LOGGER.info(
                            "Limiting worker threads to %d (configured=%d) based on available email resources.",
                            effective_thread_count,
                            configured_thread_count,
                        )
                    else:
                        LOGGER.info(
                            "Starting %d worker thread(s).", effective_thread_count
                        )
                else:
                    LOGGER.info(
                        "Iteration %d: restarting %d worker thread(s).",
                        iteration,
                        effective_thread_count,
                    )

                if is_global_shutdown_requested():
                    shutdown_requested = True

                if shutdown_requested:
                    break

                _wait_for_anymessage_resources(config)

                threads = start_worker_threads(config, effective_thread_count)
                worker_id_counter = itertools.count(len(threads) + 1)
                interrupted = _wait_for_threads(
                    config,
                    threads,
                    worker_id_counter,
                    configured_thread_count=configured_thread_count,
                )
                if is_global_shutdown_requested():
                    if not shutdown_requested:
                        reason = get_global_shutdown_reason()
                        if reason:
                            LOGGER.error(
                                "Global shutdown requested; terminating loop: %s",
                                reason,
                            )
                        else:
                            LOGGER.error(
                                "Global shutdown requested; terminating loop."
                            )
                    shutdown_requested = True
                if interrupted and not shutdown_requested:
                    LOGGER.info(
                        "Keyboard interrupt received; waiting for current worker threads to finish."
                    )
                if interrupted:
                    shutdown_requested = True

                threads = []
                worker_id_counter = None

                if shutdown_requested:
                    break

                if not restart_on_completion:
                    break

                LOGGER.info(
                    "All worker threads finished; restarting because debug mode is disabled."
                )
        except KeyboardInterrupt:
            if not shutdown_requested:
                LOGGER.info(
                    "Keyboard interrupt received; waiting for current worker threads to finish."
                )
            shutdown_requested = True
        finally:
            if threads:
                if worker_id_counter is None:
                    worker_id_counter = itertools.count(len(threads) + 1)
                if (
                    _wait_for_threads(
                        config,
                        threads,
                        worker_id_counter,
                        configured_thread_count=configured_thread_count,
                        allow_restart=False,
                    )
                    and not shutdown_requested
                ):
                    LOGGER.info(
                        "Keyboard interrupt received; waiting for current worker threads to finish."
                    )
                if is_global_shutdown_requested():
                    if not shutdown_requested:
                        reason = get_global_shutdown_reason()
                        if reason:
                            LOGGER.error(
                                "Global shutdown requested; terminating loop: %s",
                                reason,
                            )
                        else:
                            LOGGER.error(
                                "Global shutdown requested; terminating loop."
                            )
                    shutdown_requested = True
                threads = []
    finally:
        if monitor is not None:
            monitor.stop()

        # Stop metrics display
        stop_metrics_display()

    if shutdown_requested:
        LOGGER.info("Thread loop ended; shutting down.")

    if is_debug_logging_enabled(config):
        stats = get_mail_stats()
        total_errors = stats.order_failures + stats.check_failures
        total_successes = stats.order_successes + stats.check_successes
        LOGGER.info(
            (
                "Mail API stats (debug): order_attempt=%d, order_success=%d, "
                "order_failure=%d, mail_fetch_attempt=%d, mail_fetch_success=%d, "
                "mail_fetch_failure=%d, mails_received=%d, error_count=%d, "
                "successful_operations=%d"
            ),
            stats.order_attempts,
            stats.order_successes,
            stats.order_failures,
            stats.check_attempts,
            stats.check_successes,
            stats.check_failures,
            stats.check_successes,
            total_errors,
            total_successes,
        )

if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        LOGGER.info("Keyboard interrupt received; shutting down safely.")
