# -*- coding: utf-8 -*-

"""
Naver Session Check Worker

역할:
- 중개사별 네이버 세션 파일 사전 점검
- blog.naver.com / 글쓰기 URL 접근 테스트
- 로그인 페이지로 이동하면 expired 처리
- 정상 접근이면 linked 처리
- --auto-relogin 옵션 사용 시 저장된 ID/PW로 자동 재로그인 시도

사용 예:
python workers/naver_session_check_worker.py --limit 50
python workers/naver_session_check_worker.py --realtor-id 1
python workers/naver_session_check_worker.py --limit 50 --auto-relogin
python workers/naver_session_check_worker.py --limit 50 --headful

권장 운영:
1) publish_worker 실행 전
python workers/naver_session_check_worker.py --limit 100 --auto-relogin

2) publish_worker 실행
python workers/publish_worker.py --limit 100 --sleep 60 --until-hour 6

3) publish_worker 실행 후
python workers/naver_session_check_worker.py --limit 100
"""

import os
import sys
import json
import time
import argparse
import traceback
from datetime import datetime

from playwright.sync_api import sync_playwright

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 config import STORAGE_DIR

try:
    from config import APP_CRYPTO_KEY
except Exception:
    APP_CRYPTO_KEY = ""

try:
    from Crypto.Cipher import AES
    CRYPTO_AVAILABLE = True
except Exception:
    AES = None
    CRYPTO_AVAILABLE = False

from db import get_conn


SESSION_DIR = os.path.join(STORAGE_DIR, "naver_sessions")
os.makedirs(SESSION_DIR, exist_ok=True)


def log(message):
    print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {message}")


def normalize_aes_key(key_text):
    key = str(key_text or "").encode("utf-8")
    if len(key) >= 32:
        return key[:32]
    return key.ljust(32, b"\0")


def php_openssl_decrypt(encrypted_text):
    if not encrypted_text:
        return ""

    if not CRYPTO_AVAILABLE:
        raise Exception("pycryptodome 설치 필요: pip install pycryptodome")

    import base64

    raw = str(encrypted_text or "").strip()

    try:
        raw_bytes = base64.b64decode(raw, validate=True)
    except Exception:
        return ""

    if len(raw_bytes) <= 16:
        return ""

    iv = raw_bytes[:16]
    cipher_text = raw_bytes[16:]

    cipher = AES.new(
        normalize_aes_key(APP_CRYPTO_KEY),
        AES.MODE_CBC,
        iv
    )

    decrypted = cipher.decrypt(cipher_text)
    pad_len = decrypted[-1]

    if pad_len < 1 or pad_len > 16:
        return ""

    return decrypted[:-pad_len].decode("utf-8", errors="ignore")


def get_session_file(realtor_id):
    return os.path.join(
        SESSION_DIR,
        f"naver_session_{realtor_id}.json"
    )


def is_login_page_url(url):
    url = str(url or "").lower()

    return (
        "nid.naver.com" in url
        or "nidlogin.login" in url
        or ("mode=form" in url and "login" in url)
    )


def is_probably_logged_in_by_cookies(context):
    try:
        cookies = context.cookies()
        names = [
            c.get("name", "")
            for c in cookies
            if "naver.com" in c.get("domain", "")
        ]

        return ("NID_AUT" in names) or ("NID_SES" in names)

    except Exception:
        return False


def detect_login_fail_reason(page):
    try:
        page_text = page.locator("body").inner_text(timeout=5000)
    except Exception:
        page_text = ""

    page_text_lower = page_text.lower()
    current_url = str(page.url or "")

    if (
        "2단계 인증" in page_text
        or "2차 인증" in page_text
        or "otp" in page_text_lower
        or "인증번호" in page_text
    ):
        return "2차 인증 필요"

    if (
        "새로운 환경에서 로그인" in page_text
        or "본인 확인" in page_text
        or "새 기기" in page_text
    ):
        return "새 기기 로그인 인증 필요"

    if (
        "자동입력 방지문자" in page_text
        or "captcha" in page_text_lower
        or "보안문자" in page_text
    ):
        return "캡차 발생"

    if (
        "비밀번호가 일치하지 않습니다" in page_text
        or "아이디 또는 비밀번호" in page_text
    ):
        return "아이디 또는 비밀번호 오류"

    if (
        "해외" in page_text
        or "지역" in page_text
        or "로그인 차단" in page_text
    ):
        return "타지역 로그인 제한"

    if "nid.naver.com" in current_url.lower():
        return f"로그인 미완료 / current_url={current_url}"

    return f"로그인 실패 원인 미확인 / current_url={current_url}"


def classify_naver_login_status(message, default_status="expired"):
    """
    STEP96:
    네이버 자동 재로그인 실패 원인을 운영 상태값으로 분류한다.
    - password_error: 네이버 비밀번호 변경/오입력 가능성. 관리자 비밀번호 재입력 필요.
    - naver_block: 2차인증/보호조치/캡차/타지역 차단 등 수동 확인 필요.
    - expired: 일반 세션 만료/로그인 미완료.
    """
    text = str(message or "")

    if (
        "아이디 또는 비밀번호" in text
        or "비밀번호가 일치" in text
        or "비밀번호 오류" in text
        or "비밀번호 복호화" in text
        or "비밀번호 암호화 값 없음" in text
        or "비밀번호 복호화 결과 없음" in text
    ):
        return "password_error"

    if (
        "2차 인증" in text
        or "2단계 인증" in text
        or "otp" in text.lower()
        or "인증번호" in text
        or "본인 확인" in text
        or "새 기기" in text
        or "새로운 환경" in text
        or "캡차" in text
        or "captcha" in text.lower()
        or "보안문자" in text
        or "타지역" in text
        or "로그인 차단" in text
        or "보호조치" in text
    ):
        return "naver_block"

    return default_status


def get_target_realtors(realtor_id=None, limit=50, only_enabled=True):
    """
    세션 점검 대상 중개사 조회.

    운영 조건:
    - 중개사 active
    - 자동발행 ON
    - 네이버 자동발행 동의 agreed
    - 유료회원 active
    - 유료기간 내
    - 상품: 네이버블로그자동화(blog_auto) 또는 패키지(package)
    - AI숏츠 단독 상품은 제외

    only_enabled=False 또는 --include-disabled 사용 시에는
    단일 realtor-id 점검/디버깅을 위해 기존처럼 넓게 조회할 수 있다.
    """
    conn = get_conn()

    try:
        params = []

        sql = """
            SELECT
                r.id AS realtor_id,
                r.office_name,
                COALESCE(a.naver_id, '') AS naver_id,
                COALESCE(a.blog_id, a.naver_id, '') AS blog_id,
                COALESCE(a.naver_password_enc, '') AS naver_password_enc,
                COALESCE(a.consent_status, '') AS consent_status,
                COALESCE(a.login_status, '') AS login_status,
                COALESCE(ps.session_status, '') AS publish_session_status,
                COALESCE(ps.auto_publish_enabled, 0) AS auto_publish_enabled
            FROM blog_realtors r
            LEFT JOIN blog_naver_accounts a
                ON a.realtor_id = r.id
            LEFT JOIN blog_publish_settings ps
                ON ps.realtor_id = r.id
            WHERE 1=1
        """

        if realtor_id:
            sql += " AND r.id = %s "
            params.append(int(realtor_id))

        if only_enabled:
            sql += """
                AND r.status = 'active'
                AND ps.auto_publish_enabled = 1
                AND a.consent_status = 'agreed'
                AND COALESCE(a.naver_id, '') <> ''
                AND COALESCE(a.naver_password_enc, '') <> ''
                AND COALESCE(a.login_status, '') NOT IN ('password_error', 'naver_block')
                AND EXISTS (
                    SELECT 1
                    FROM blog_paid_memberships pm
                    WHERE pm.realtor_id = r.id
                      AND pm.membership_status = 'active'
                      AND pm.payment_status IN ('paid', 'partial')
                      AND pm.service_type IN ('blog_auto', 'package')
                      AND pm.start_date IS NOT NULL
                      AND pm.start_date <= CURDATE()
                      AND COALESCE(pm.end_date, pm.auto_end_date) IS NOT NULL
                      AND COALESCE(pm.end_date, pm.auto_end_date) >= CURDATE()
                    LIMIT 1
                )
            """

        sql += """
            ORDER BY r.id ASC
            LIMIT %s
        """
        params.append(int(limit))

        with conn.cursor() as cur:
            cur.execute(sql, params)
            rows = cur.fetchall()

        return rows or []

    finally:
        conn.close()

def update_naver_session_status(realtor_id, status, message=None, session_file=None, naver_id=None):
    conn = get_conn()

    try:
        session_json = ""

        if session_file and os.path.exists(session_file):
            try:
                with open(session_file, "r", encoding="utf-8") as f:
                    session_json = f.read()
            except Exception:
                session_json = ""

        if not naver_id:
            try:
                with conn.cursor() as cur:
                    cur.execute("""
                        SELECT naver_id
                        FROM blog_naver_accounts
                        WHERE realtor_id = %s
                        LIMIT 1
                    """, (realtor_id,))
                    row = cur.fetchone()
                    naver_id = row.get("naver_id") if row else None
            except Exception:
                naver_id = None

        with conn.cursor() as cur:
            if status == "linked":
                cur.execute("""
                    INSERT INTO blog_naver_sessions
                    (
                        realtor_id,
                        naver_id,
                        session_status,
                        session_data,
                        issued_at,
                        last_verified_at,
                        fail_count,
                        created_at,
                        updated_at
                    )
                    VALUES
                    (
                        %s,
                        %s,
                        'linked',
                        %s,
                        NOW(),
                        NOW(),
                        0,
                        NOW(),
                        NOW()
                    )
                """, (realtor_id, naver_id, session_json))

                cur.execute("""
                    UPDATE blog_publish_settings
                    SET
                        session_status = 'linked',
                        session_last_checked_at = NOW(),
                        updated_at = NOW()
                    WHERE realtor_id = %s
                """, (realtor_id,))

                cur.execute("""
                    UPDATE blog_naver_accounts
                    SET
                        login_status = 'linked',
                        last_login_at = NOW(),
                        last_login_error = NULL,
                        updated_at = NOW()
                    WHERE realtor_id = %s
                """, (realtor_id,))

            else:
                cur.execute("""
                    INSERT INTO blog_naver_sessions
                    (
                        realtor_id,
                        naver_id,
                        session_status,
                        session_data,
                        last_verified_at,
                        fail_count,
                        created_at,
                        updated_at
                    )
                    VALUES
                    (
                        %s,
                        %s,
                        %s,
                        %s,
                        NOW(),
                        1,
                        NOW(),
                        NOW()
                    )
                """, (realtor_id, naver_id, status, session_json))

                cur.execute("""
                    UPDATE blog_publish_settings
                    SET
                        session_status = %s,
                        session_last_checked_at = NOW(),
                        updated_at = NOW()
                    WHERE realtor_id = %s
                """, (status, realtor_id))

                cur.execute("""
                    UPDATE blog_naver_accounts
                    SET
                        login_status = %s,
                        last_login_error = %s,
                        updated_at = NOW()
                    WHERE realtor_id = %s
                """, (status, str(message or "")[:1000], realtor_id))

        conn.commit()

    finally:
        conn.close()


def get_decrypted_password(row):
    encrypted_pw = str(row.get("naver_password_enc") or "").strip()

    if not encrypted_pw:
        return "", "네이버 비밀번호 암호화 값 없음"

    try:
        naver_pw = php_openssl_decrypt(encrypted_pw)
    except Exception as e:
        return "", f"비밀번호 복호화 실패: {e}"

    if not naver_pw:
        return "", "비밀번호 복호화 결과 없음"

    return naver_pw, ""


def auto_relogin(context, page, row, session_file, target_url="https://blog.naver.com"):
    realtor_id = int(row.get("realtor_id") or 0)
    naver_id = str(row.get("naver_id") or "").strip()

    if str(row.get("consent_status") or "").strip() != "agreed":
        return False, "자동발행 동의 상태가 아닙니다."

    if not naver_id:
        return False, "네이버 ID 없음"

    naver_pw, pw_error = get_decrypted_password(row)

    if pw_error:
        return False, pw_error

    try:
        from urllib.parse import quote

        login_url = "https://nid.naver.com/nidlogin.login?mode=form&url=" + quote(target_url, safe="")

        log(f"[AUTO RELOGIN START] realtor_id={realtor_id}, naver_id={naver_id}")
        page.goto(login_url, wait_until="domcontentloaded", timeout=60000)
        page.wait_for_timeout(1200)

        page.evaluate("""
            ([naverId, naverPw]) => {
                const id = document.querySelector('#id');
                const pw = document.querySelector('#pw');

                if (id) {
                    id.focus();
                    id.value = naverId;
                    id.dispatchEvent(new Event('input', { bubbles: true }));
                    id.dispatchEvent(new Event('change', { bubbles: true }));
                }

                if (pw) {
                    pw.focus();
                    pw.value = naverPw;
                    pw.dispatchEvent(new Event('input', { bubbles: true }));
                    pw.dispatchEvent(new Event('change', { bubbles: true }));
                }
            }
        """, [naver_id, naver_pw])

        page.wait_for_timeout(600)

        clicked = False

        for selector in ["#log\\.login", "button:has-text('로그인')", "input[type='submit']"]:
            try:
                loc = page.locator(selector).first
                if loc.count() > 0:
                    loc.click(force=True, timeout=5000)
                    clicked = True
                    break
            except Exception:
                continue

        if not clicked:
            page.keyboard.press("Enter")

        login_success = False

        for i in range(90):
            page.wait_for_timeout(1000)

            if is_probably_logged_in_by_cookies(context):
                login_success = True
                break

            if i in [5, 15, 30, 60]:
                log(f"[AUTO RELOGIN WAIT] realtor_id={realtor_id}, {i}/90, url={page.url}")

        if not login_success:
            reason = detect_login_fail_reason(page)
            return False, reason

        context.storage_state(path=session_file)
        return True, "자동 재로그인 성공"

    except Exception as e:
        return False, f"자동 재로그인 예외: {e}"


def check_one_realtor(playwright, row, headless=True, auto_relogin_enabled=False, write_check=True):
    realtor_id = int(row.get("realtor_id") or 0)
    naver_id = str(row.get("naver_id") or "").strip()
    blog_id = str(row.get("blog_id") or naver_id or "").strip()
    office_name = str(row.get("office_name") or "").strip()
    session_file = get_session_file(realtor_id)

    log("-" * 80)
    log(f"[CHECK START] realtor_id={realtor_id}, office={office_name}, naver_id={naver_id}, blog_id={blog_id}")

    if not os.path.exists(session_file):
        message = f"세션 파일 없음: {session_file}"

        update_naver_session_status(
            realtor_id,
            "missing",
            message,
            session_file=session_file,
            naver_id=naver_id
        )

        log(f"[SESSION MISSING] realtor_id={realtor_id} / {message}")

        if not auto_relogin_enabled:
            return False, "missing", message

    browser = None

    try:
        browser = playwright.chromium.launch(
            headless=headless,
            args=[
                "--disable-blink-features=AutomationControlled",
                "--no-sandbox",
            ]
        )

        context_kwargs = {
            "locale": "ko-KR",
            "viewport": {
                "width": 1280,
                "height": 900,
            }
        }

        if os.path.exists(session_file):
            context_kwargs["storage_state"] = session_file

        context = browser.new_context(**context_kwargs)
        page = context.new_page()

        page.goto("https://blog.naver.com", wait_until="domcontentloaded", timeout=60000)
        page.wait_for_timeout(1500)

        log(f"[BLOG HOME URL] realtor_id={realtor_id}, url={page.url}")

        if is_login_page_url(page.url) or not is_probably_logged_in_by_cookies(context):
            message = f"블로그 홈 세션 만료 감지: {page.url}"

            if auto_relogin_enabled:
                ok, relogin_message = auto_relogin(
                    context=context,
                    page=page,
                    row=row,
                    session_file=session_file,
                    target_url="https://blog.naver.com"
                )

                if not ok:
                    classified_status = classify_naver_login_status(relogin_message, default_status="expired")
                    update_naver_session_status(
                        realtor_id,
                        classified_status,
                        f"{message} / 자동복구 실패: {relogin_message}",
                        session_file=session_file,
                        naver_id=naver_id
                    )

                    log(f"[SESSION EXPIRED] realtor_id={realtor_id} / status={classified_status} / auto_relogin_failed={relogin_message}")
                    browser.close()
                    return False, classified_status, relogin_message

                log(f"[AUTO RELOGIN OK] realtor_id={realtor_id}")

                page.goto("https://blog.naver.com", wait_until="domcontentloaded", timeout=60000)
                page.wait_for_timeout(1500)

            else:
                update_naver_session_status(
                    realtor_id,
                    "expired",
                    message,
                    session_file=session_file,
                    naver_id=naver_id
                )

                log(f"[SESSION EXPIRED] realtor_id={realtor_id} / {message}")
                browser.close()
                return False, "expired", message

        if write_check:
            write_url = f"https://blog.naver.com/{blog_id}?Redirect=Write&categoryNo=2" if blog_id else "https://blog.naver.com/GoBlogWrite.naver"

            page.goto(write_url, wait_until="domcontentloaded", timeout=60000)
            page.wait_for_timeout(2500)

            log(f"[WRITE CHECK URL] realtor_id={realtor_id}, url={page.url}")

            if is_login_page_url(page.url):
                message = f"글쓰기 접근 중 로그인 페이지 이동: {page.url}"

                if auto_relogin_enabled:
                    ok, relogin_message = auto_relogin(
                        context=context,
                        page=page,
                        row=row,
                        session_file=session_file,
                        target_url=write_url
                    )

                    if not ok:
                        classified_status = classify_naver_login_status(relogin_message, default_status="expired")
                        update_naver_session_status(
                            realtor_id,
                            classified_status,
                            f"{message} / 자동복구 실패: {relogin_message}",
                            session_file=session_file,
                            naver_id=naver_id
                        )

                        log(f"[WRITE SESSION EXPIRED] realtor_id={realtor_id} / status={classified_status} / auto_relogin_failed={relogin_message}")
                        browser.close()
                        return False, classified_status, relogin_message

                    page.goto(write_url, wait_until="domcontentloaded", timeout=60000)
                    page.wait_for_timeout(2500)

                    if is_login_page_url(page.url):
                        fail_message = f"자동복구 후에도 글쓰기 로그인 페이지: {page.url}"

                        update_naver_session_status(
                            realtor_id,
                            "expired",
                            fail_message,
                            session_file=session_file,
                            naver_id=naver_id
                        )

                        log(f"[WRITE SESSION STILL EXPIRED] realtor_id={realtor_id}")
                        browser.close()
                        return False, "expired", fail_message

                else:
                    update_naver_session_status(
                        realtor_id,
                        "expired",
                        message,
                        session_file=session_file,
                        naver_id=naver_id
                    )

                    log(f"[WRITE SESSION EXPIRED] realtor_id={realtor_id} / {message}")
                    browser.close()
                    return False, "expired", message

        context.storage_state(path=session_file)

        update_naver_session_status(
            realtor_id,
            "linked",
            "세션 사전 점검 성공",
            session_file=session_file,
            naver_id=naver_id
        )

        log(f"[SESSION OK] realtor_id={realtor_id}")

        browser.close()
        return True, "linked", "세션 정상"

    except Exception as e:
        traceback.print_exc()

        message = str(e)[:1000]

        update_naver_session_status(
            realtor_id,
            "error",
            message,
            session_file=session_file,
            naver_id=naver_id
        )

        log(f"[SESSION CHECK ERROR] realtor_id={realtor_id} / {message}")

        try:
            if browser:
                browser.close()
        except Exception:
            pass

        return False, "error", message


def run(limit=50, realtor_id=None, headless=True, auto_relogin_enabled=False, write_check=True, only_enabled=True, sleep_seconds=2):
    # 관리자에서 특정 중개사를 직접 점검할 때는
    # 유료회원/운영대상 필터 때문에 대상이 0건이 되지 않도록 한다.
    # 일괄 스케줄 실행은 기존 only_enabled 정책을 그대로 유지한다.
    effective_only_enabled = only_enabled if not realtor_id else False

    rows = get_target_realtors(
        realtor_id=realtor_id,
        limit=limit,
        only_enabled=effective_only_enabled
    )

    log("=" * 80)
    log(f"[SESSION CHECK TARGETS] {len(rows)}")
    log(f"[TARGET FILTER] {'OPERATING_ONLY' if effective_only_enabled else 'DIRECT_REALTOR'}")
    log(f"[AUTO RELOGIN] {'ON' if auto_relogin_enabled else 'OFF'}")
    log(f"[WRITE CHECK] {'ON' if write_check else 'OFF'}")
    log("=" * 80)

    if not rows:
        return

    ok_count = 0
    fail_count = 0
    status_counts = {}

    with sync_playwright() as p:
        for row in rows:
            ok, status, message = check_one_realtor(
                playwright=p,
                row=row,
                headless=headless,
                auto_relogin_enabled=auto_relogin_enabled,
                write_check=write_check
            )

            status_counts[status] = status_counts.get(status, 0) + 1

            if ok:
                ok_count += 1
            else:
                fail_count += 1

            if sleep_seconds and int(sleep_seconds) > 0:
                time.sleep(int(sleep_seconds))

    log("=" * 80)
    log("[SESSION CHECK DONE]")
    log(f"[OK] {ok_count}")
    log(f"[FAIL] {fail_count}")
    log(f"[STATUS COUNTS] {json.dumps(status_counts, ensure_ascii=False)}")
    log("=" * 80)


def main():
    parser = argparse.ArgumentParser()

    parser.add_argument("--limit", type=int, default=50)
    parser.add_argument("--realtor-id", type=int, default=None)
    parser.add_argument("--headful", action="store_true")
    parser.add_argument("--auto-relogin", action="store_true")
    parser.add_argument("--no-write-check", action="store_true")
    parser.add_argument("--include-disabled", action="store_true")
    parser.add_argument("--sleep", type=int, default=2)

    args = parser.parse_args()

    run(
        limit=args.limit,
        realtor_id=args.realtor_id,
        headless=not args.headful,
        auto_relogin_enabled=args.auto_relogin,
        write_check=not args.no_write_check,
        only_enabled=not args.include_disabled,
        sleep_seconds=args.sleep
    )


if __name__ == "__main__":
    main()
