"""
メイン監視ループ

DBの monitor_settings / media_settings を参照し、
設定された監視時間・間隔でスクレイピングスクリプトを呼び出す。

Usage:
    python monitor.py <client_id>
"""

import argparse
import logging
import random
import signal
import subprocess
import sys
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime, timedelta
from pathlib import Path

sys.path.append(str(Path(__file__).resolve().parents[1]))
from config.database import execute_query

# ---------------------------------------------------------------------------
# 定数
# ---------------------------------------------------------------------------
SCRAPING_DIR = Path(__file__).parent
INTERVAL_JITTER = 10  # ± 秒
WAIT_OUTSIDE_HOURS = 60  # 設定取得失敗時のリトライ間隔（秒）

MEDIA_SCRIPTS = {
    "girlsheaven": SCRAPING_DIR / "scrape_girlsheaven.py",
    "vanilla":      SCRAPING_DIR / "scrape_vanilla.py",
}

GH_ACCESS_SCRIPT = Path(__file__).resolve().parents[1] / "tools" / "get_access_girlsheaven.py"

# ---------------------------------------------------------------------------
# ロガー設定
# ---------------------------------------------------------------------------
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
)
logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# シャットダウン管理
# ---------------------------------------------------------------------------
class _ShutdownFlag:
    def __init__(self) -> None:
        self._flag = False

    def set(self) -> None:
        self._flag = True

    @property
    def is_set(self) -> bool:
        return self._flag


_shutdown = _ShutdownFlag()


def _handle_sigint(signum, frame) -> None:
    logger.info("Ctrl+C を受信しました。終了します...")
    _shutdown.set()


signal.signal(signal.SIGINT, _handle_sigint)


# ---------------------------------------------------------------------------
# DB 取得
# ---------------------------------------------------------------------------
def get_monitor_settings(client_id: int) -> dict:
    """monitor_settings テーブルから監視設定を取得する。"""
    rows = execute_query(
        """
        SELECT interval_min
        FROM monitor_settings
        WHERE client_id = %s
        LIMIT 1
        """,
        (client_id,),
    )
    if not rows:
        raise ValueError(f"monitor_settings に client_id={client_id} の設定が見つかりません")
    return rows[0]


def get_active_media_settings(client_id: int) -> list[dict]:
    """
    media_settings テーブルから有効な媒体設定を取得する。
    返値例: [
        {"media_name": "girlsheaven", "monitor_url": "...", "target_rank": 10},
        {"media_name": "vanilla",     "monitor_url": "...", "target_rank": None},
    ]
    """
    return execute_query(
        """
        SELECT media_name, monitor_url
        FROM media_settings
        WHERE client_id = %s
          AND is_active = 1
        """,
        (client_id,),
    )


# ---------------------------------------------------------------------------
# スクリプト呼び出し
# ---------------------------------------------------------------------------
def call_scraper(client_id: int, media: dict, slot_time: datetime = None) -> None:
    """
    媒体に応じたスクレイピングスクリプトをサブプロセスで呼び出す。
    """
    media_name  = media["media_name"]
    monitor_url = media["monitor_url"]

    script = MEDIA_SCRIPTS.get(media_name)
    if script is None:
        logger.warning("未対応の媒体: %s", media_name)
        return

    slot_str = slot_time.strftime('%Y-%m-%d %H:%M:%S') if slot_time else ''

    if media_name == "girlsheaven":
        cmd = [sys.executable, str(script), str(client_id), monitor_url, slot_str]

    elif media_name == "vanilla":
        cmd = [sys.executable, str(script), str(client_id), monitor_url, slot_str]

    else:
        logger.warning("未対応の媒体: %s", media_name)
        return

    logger.info("実行: %s", " ".join(cmd))
    try:
        result = subprocess.run(cmd, check=True, text=True, capture_output=False)
        logger.info("%s 完了 (returncode=%d)", media_name, result.returncode)
    except subprocess.CalledProcessError as e:
        logger.error("%s 失敗 (returncode=%d)", media_name, e.returncode)
    except Exception as e:
        logger.error("%s 呼び出しエラー: %s", media_name, e)


def _should_fetch_gh_access(client_id: int) -> bool:
    """00:00〜04:00の間のランダムな時刻にGHアクセス統計を取得すべきか判定する。
    1日1回だけ実行する。
    """
    now = datetime.now()
    if not (0 <= now.hour < 4):
        return False

    # 本日分がすでに取得済みかチェック
    try:
        from config.database import execute_query as _eq
        rows = _eq(
            "SELECT COUNT(*) as cnt FROM gh_access_stats WHERE client_id = %s AND stat_date = %s",
            (client_id, now.date()),
            local=True,
        )
        if rows and rows[0]["cnt"] > 0:
            return False
    except Exception:
        return False

    # ランダムな確率で実行（1ループあたり約3分、4時間=80ループで1回実行）
    # 確率 = 1/80 ≈ 1.25%
    return random.random() < (1 / 80)


def _run_gh_access(client_id: int) -> None:
    """get_access_girlsheaven.pyを実行する。"""
    cmd = [sys.executable, str(GH_ACCESS_SCRIPT), str(client_id)]
    logger.info("GHアクセス統計取得を実行します")
    try:
        result = subprocess.run(cmd, check=True, text=True, capture_output=False)
        logger.info("GHアクセス統計取得完了 (returncode=%d)", result.returncode)
    except Exception as e:
        logger.error("GHアクセス統計取得エラー: %s", e)


# ---------------------------------------------------------------------------
# メインループ
# ---------------------------------------------------------------------------
def run_loop(client_id: int) -> None:
    logger.info("監視ループ開始 | client_id=%d", client_id)

    while not _shutdown.is_set:
        # GHアクセス統計の自動取得（00:00〜04:00のランダムな時刻に1回）
        if _should_fetch_gh_access(client_id):
            _run_gh_access(client_id)

        # ── 設定を毎ループ再取得（DB変更をリアルタイム反映）──────────────────
        try:
            settings = get_monitor_settings(client_id)
            media_list = get_active_media_settings(client_id)
        except Exception as e:
            logger.error("設定取得エラー: %s", e)
            time.sleep(WAIT_OUTSIDE_HOURS)
            continue

        interval_min = int(settings["interval_min"])

        # ── 各媒体を並列実行 ──────────────────────────────────────────────────
        if not media_list:
            logger.warning("有効な媒体設定がありません (client_id=%d)", client_id)
        else:
            if _shutdown.is_set:
                break
            # 定刻スロット時刻を記録
            slot_time = _get_slot_time(interval_min)
            logger.info("定刻スロット: %s", slot_time.strftime('%H:%M:%S'))

            # ランダム遅延（0〜20秒）でブラウザ起動
            random_delay = random.randint(0, 20)
            logger.info("ブラウザ起動まで %d秒 待機します", random_delay)
            _interruptible_sleep(random_delay)

            with ThreadPoolExecutor(max_workers=2) as executor:
                futures = {
                    executor.submit(call_scraper, client_id, media, slot_time): media.get("media_name")
                    for media in media_list
                }
                for future in as_completed(futures):
                    media_name = futures[future]
                    try:
                        future.result()
                    except Exception as e:
                        logger.error("[%s] スクレイピングエラー: %s", media_name, e)

        if _shutdown.is_set:
            break

        # ── 次の定刻スロットまで待機 ──────────────────────────────────────────
        wait_sec = _next_slot_wait(interval_min)
        logger.info(
            "次の定刻スロットまで %d秒 待機します（次回実行: %s）",
            wait_sec,
            (datetime.now() + timedelta(seconds=wait_sec)).strftime('%H:%M:%S'),
        )
        _interruptible_sleep(wait_sec)

    logger.info("監視ループを終了しました。")


def _get_slot_time(interval_min: int) -> datetime:
    """現在時刻を切り捨てて定刻スロット時刻を返す。
    例：09:01:30に呼ばれた場合、interval_min=3なら09:00:00を返す。
    """
    now = datetime.now()
    slot_min = (now.minute // interval_min) * interval_min
    return now.replace(minute=slot_min, second=0, microsecond=0)


def _next_slot_wait(interval_min: int) -> int:
    """次のinterval_min刻みの定刻までの待機秒数を返す。
    例：interval_min=3の場合、00,03,06,09...分に実行されるよう待機秒数を計算する。
    """
    now = datetime.now()
    current_total_sec = now.minute * 60 + now.second
    slot_sec = interval_min * 60
    next_slot = (current_total_sec // slot_sec + 1) * slot_sec
    wait_sec = next_slot - current_total_sec
    return wait_sec


def _interruptible_sleep(seconds: int) -> None:
    """Ctrl+C を即時に受け付けながら指定秒数スリープする。"""
    end = time.monotonic() + seconds
    while not _shutdown.is_set and time.monotonic() < end:
        time.sleep(1)


# ---------------------------------------------------------------------------
# エントリポイント
# ---------------------------------------------------------------------------
def main() -> None:
    parser = argparse.ArgumentParser(description="順位監視メインループ")
    parser.add_argument("client_id", type=int, help="クライアントID")
    args = parser.parse_args()

    try:
        run_loop(args.client_id)
    except Exception as e:
        logger.critical("予期せぬエラーで終了: %s", e)
        sys.exit(1)


if __name__ == "__main__":
    main()
