# -*- coding: utf-8 -*-
"""
STEP121-1 resume_manager.py

목적:
- 서버 재부팅/강제 종료/브라우저 종료 등으로 running/processing 상태에 멈춘 작업을
  안전하게 복구한다.
- 이 파일은 복구만 수행한다. publish_worker/schedule_daily_articles/generate_blog_drafts를 실행하지 않는다.

사용:
    python workers/resume_manager.py

Dry-run:
    python workers/resume_manager.py --dry-run

주의:
- 운영 적용 전에는 반드시 --dry-run으로 먼저 확인한다.
- 기존 파일 수정 없음. 단독 실행형 복구 도구다.
"""

import os
import sys
import argparse
from datetime import datetime

CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
ROOT_DIR = os.path.dirname(CURRENT_DIR)

if ROOT_DIR not in sys.path:
    sys.path.append(ROOT_DIR)

from db import get_conn


def print_line():
    print("=" * 80)


def fetch_all(sql, params=None):
    conn = get_conn()
    try:
        with conn.cursor() as cur:
            cur.execute(sql, params or [])
            return cur.fetchall() or []
    finally:
        conn.close()


def execute(sql, params=None, dry_run=False):
    if dry_run:
        print("[DRY RUN SQL]", " ".join(sql.split()))
        print("[DRY RUN PARAMS]", params or [])
        return 0

    conn = get_conn()
    try:
        with conn.cursor() as cur:
            cur.execute(sql, params or [])
            affected = cur.rowcount
        conn.commit()
        return affected
    finally:
        conn.close()


def recover_pipeline_runs(minutes=30, dry_run=False):
    rows = fetch_all("""
        SELECT
            id, run_type, status, started_at,
            TIMESTAMPDIFF(MINUTE, started_at, NOW()) AS running_minutes,
            message
        FROM blog_pipeline_runs
        WHERE status = 'running'
          AND started_at IS NOT NULL
          AND started_at < DATE_SUB(NOW(), INTERVAL %s MINUTE)
        ORDER BY id ASC
    """, [int(minutes)])

    print("[PIPELINE STALE RUNNING]", len(rows))

    for r in rows:
        print(
            " -",
            f"id={r.get('id')}",
            f"run_type={r.get('run_type')}",
            f"minutes={r.get('running_minutes')}",
            f"started_at={r.get('started_at')}",
        )

    affected = execute("""
        UPDATE blog_pipeline_runs
        SET
            status = 'stopped',
            finished_at = NOW(),
            message = CONCAT('[RESUME MANAGER] stale running recovered. ', IFNULL(message, ''))
        WHERE status = 'running'
          AND started_at IS NOT NULL
          AND started_at < DATE_SUB(NOW(), INTERVAL %s MINUTE)
    """, [int(minutes)], dry_run=dry_run)

    print("[PIPELINE RECOVERED]", affected)
    return len(rows), affected


def recover_publish_queue(minutes=90, dry_run=False):
    rows = fetch_all("""
        SELECT
            id, realtor_id, draft_id, article_no,
            queue_status, worker_status, started_at,
            TIMESTAMPDIFF(MINUTE, started_at, NOW()) AS processing_minutes,
            error_message
        FROM blog_publish_queue
        WHERE queue_status = 'processing'
          AND started_at IS NOT NULL
          AND started_at < DATE_SUB(NOW(), INTERVAL %s MINUTE)
        ORDER BY id ASC
    """, [int(minutes)])

    print("[PUBLISH QUEUE STALE PROCESSING]", len(rows))

    for r in rows:
        print(
            " -",
            f"id={r.get('id')}",
            f"realtor_id={r.get('realtor_id')}",
            f"article_no={r.get('article_no')}",
            f"minutes={r.get('processing_minutes')}",
        )

    affected = execute("""
        UPDATE blog_publish_queue
        SET
            queue_status = 'pending',
            worker_status = 'resume_recovered',
            started_at = NULL,
            finished_at = NULL,
            error_message = CONCAT('[RESUME MANAGER] stale processing recovered. ', IFNULL(error_message, '')),
            updated_at = NOW()
        WHERE queue_status = 'processing'
          AND started_at IS NOT NULL
          AND started_at < DATE_SUB(NOW(), INTERVAL %s MINUTE)
    """, [int(minutes)], dry_run=dry_run)

    print("[PUBLISH QUEUE RECOVERED]", affected)
    return len(rows), affected


def recover_private_queue(minutes=90, dry_run=False):
    rows = fetch_all("""
        SELECT
            id, realtor_id, article_no, draft_id,
            queue_status, worker_status, started_at,
            TIMESTAMPDIFF(MINUTE, started_at, NOW()) AS processing_minutes,
            error_message
        FROM blog_post_private_queue
        WHERE queue_status = 'processing'
          AND started_at IS NOT NULL
          AND started_at < DATE_SUB(NOW(), INTERVAL %s MINUTE)
        ORDER BY id ASC
    """, [int(minutes)])

    print("[PRIVATE QUEUE STALE PROCESSING]", len(rows))

    for r in rows:
        print(
            " -",
            f"id={r.get('id')}",
            f"realtor_id={r.get('realtor_id')}",
            f"article_no={r.get('article_no')}",
            f"minutes={r.get('processing_minutes')}",
        )

    affected = execute("""
        UPDATE blog_post_private_queue
        SET
            queue_status = 'pending',
            worker_status = 'resume_recovered',
            started_at = NULL,
            finished_at = NULL,
            error_message = CONCAT('[RESUME MANAGER] stale processing recovered. ', IFNULL(error_message, '')),
            updated_at = NOW()
        WHERE queue_status = 'processing'
          AND started_at IS NOT NULL
          AND started_at < DATE_SUB(NOW(), INTERVAL %s MINUTE)
    """, [int(minutes)], dry_run=dry_run)

    print("[PRIVATE QUEUE RECOVERED]", affected)
    return len(rows), affected


def recover_app_locks(minutes=30, dry_run=False):
    expired_rows = fetch_all("""
        SELECT lock_name, locked_at, expires_at, owner_token
        FROM blog_app_locks
        WHERE expires_at < NOW()
        ORDER BY locked_at ASC
    """)

    print("[APP LOCK EXPIRED]", len(expired_rows))

    for r in expired_rows:
        print(
            " - expired",
            f"lock_name={r.get('lock_name')}",
            f"locked_at={r.get('locked_at')}",
            f"expires_at={r.get('expires_at')}",
        )

    expired_affected = execute("""
        DELETE FROM blog_app_locks
        WHERE expires_at < NOW()
    """, [], dry_run=dry_run)

    stale_rows = fetch_all("""
        SELECT lock_name, locked_at, expires_at, owner_token,
               TIMESTAMPDIFF(MINUTE, locked_at, NOW()) AS locked_minutes
        FROM blog_app_locks
        WHERE locked_at IS NOT NULL
          AND locked_at < DATE_SUB(NOW(), INTERVAL %s MINUTE)
        ORDER BY locked_at ASC
    """, [int(minutes)])

    print("[APP LOCK STALE]", len(stale_rows))

    for r in stale_rows:
        print(
            " - stale",
            f"lock_name={r.get('lock_name')}",
            f"minutes={r.get('locked_minutes')}",
            f"owner={r.get('owner_token')}",
        )

    stale_affected = execute("""
        DELETE FROM blog_app_locks
        WHERE locked_at IS NOT NULL
          AND locked_at < DATE_SUB(NOW(), INTERVAL %s MINUTE)
    """, [int(minutes)], dry_run=dry_run)

    print("[APP LOCK RECOVERED]", expired_affected + stale_affected)
    return len(expired_rows) + len(stale_rows), expired_affected + stale_affected


def print_current_summary():
    print_line()
    print("[CURRENT SUMMARY]")

    queries = [
        ("pipeline_running", """
            SELECT COUNT(*) AS cnt
            FROM blog_pipeline_runs
            WHERE status = 'running'
        """),
        ("publish_processing", """
            SELECT COUNT(*) AS cnt
            FROM blog_publish_queue
            WHERE queue_status = 'processing'
        """),
        ("publish_pending", """
            SELECT COUNT(*) AS cnt
            FROM blog_publish_queue
            WHERE queue_status = 'pending'
        """),
        ("private_processing", """
            SELECT COUNT(*) AS cnt
            FROM blog_post_private_queue
            WHERE queue_status = 'processing'
        """),
        ("private_pending", """
            SELECT COUNT(*) AS cnt
            FROM blog_post_private_queue
            WHERE queue_status = 'pending'
        """),
        ("app_locks", """
            SELECT COUNT(*) AS cnt
            FROM blog_app_locks
        """),
    ]

    for name, sql in queries:
        try:
            rows = fetch_all(sql)
            row = rows[0] if rows else {}
            print(f" {name}: {int(row.get('cnt') or 0)}")
        except Exception as e:
            print(f" {name}: ERROR {e}")


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--dry-run", action="store_true", help="변경 없이 대상만 출력")
    parser.add_argument("--pipeline-minutes", type=int, default=30)
    parser.add_argument("--publish-minutes", type=int, default=90)
    parser.add_argument("--private-minutes", type=int, default=90)
    parser.add_argument("--lock-minutes", type=int, default=30)
    parser.add_argument("--skip-locks", action="store_true")
    parser.add_argument("--skip-pipeline", action="store_true")
    parser.add_argument("--skip-publish", action="store_true")
    parser.add_argument("--skip-private", action="store_true")
    args = parser.parse_args()

    print_line()
    print("STEP121-1 RESUME MANAGER")
    print("time:", datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
    print("dry_run:", args.dry_run)
    print_line()

    print_current_summary()
    print_line()

    total_targets = 0
    total_recovered = 0

    if not args.skip_pipeline:
        targets, recovered = recover_pipeline_runs(args.pipeline_minutes, args.dry_run)
        total_targets += targets
        total_recovered += recovered
        print_line()

    if not args.skip_publish:
        targets, recovered = recover_publish_queue(args.publish_minutes, args.dry_run)
        total_targets += targets
        total_recovered += recovered
        print_line()

    if not args.skip_private:
        targets, recovered = recover_private_queue(args.private_minutes, args.dry_run)
        total_targets += targets
        total_recovered += recovered
        print_line()

    if not args.skip_locks:
        targets, recovered = recover_app_locks(args.lock_minutes, args.dry_run)
        total_targets += targets
        total_recovered += recovered
        print_line()

    print_current_summary()
    print_line()

    print("[RESUME MANAGER DONE]")
    print("[TARGETS]", total_targets)
    print("[RECOVERED]", total_recovered)
    print_line()


if __name__ == "__main__":
    main()
