"""X (Twitter) motoru — twikit ile cookie tabanlı erişim.

- Hesap doğrulama (login testi), tweet atma, hesap rotasyonu.
- twikit async'tir; scheduler thread'lerinden asyncio.run() ile köprülenir.
- Ban/askı tespitinde hesap otomatik devre dışı bırakılır, panel uyarır.
"""
import asyncio
import logging
from datetime import datetime

from sqlalchemy.orm import Session
from twikit import Client
from twikit.errors import (
    AccountSuspended, AccountLocked, Unauthorized, Forbidden, TooManyRequests,
    DuplicateTweet, raise_exceptions_from_response,
)

from app.db import models, crud
from app.core import timeutil
from app.x_engine import twikit_patch

log = logging.getLogger("x_engine")

# X'in değişen JS paket formatı yüzünden twikit'in transaction-id üretimi
# kırık; yama import anında uygulanır (bkz. twikit_patch modül açıklaması).
twikit_patch.apply()

USER_AGENT = (
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
    "(KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
)


def _build_client(account: models.Account) -> Client:
    cookies = crud.account_cookies(account)
    client = Client("tr")
    client.set_cookies({"auth_token": cookies["auth_token"], "ct0": cookies["ct0"]})
    client.http.headers["User-Agent"] = USER_AGENT
    return client


def _run(coro):
    """Async twikit çağrısını sync dünyadan çalıştır."""
    return asyncio.run(coro)


# ------------------------------------------------------------------ doğrulama
async def _verify(client: Client):
    """Oturumun geçerli olduğunu kanıtla.

    NOT: `client.user()` KULLANILMAZ. twikit 2.3.3'te o metot X'in kaldırdığı
    v1.1 `account/settings` uç noktasına gider (404) ve kullanıcı modeli
    ayrıştırması da `KeyError: 'urls'` veriyor (X alan yapısını değiştirdi).
    Kimlik doğrulamayı kanıtlamak için GraphQL tabanlı zaman tüneli çağrısı
    yeterli: cookie geçersizse Unauthorized/Forbidden fırlatır.
    """
    await client.get_latest_timeline(count=1)
    return True


def verify_account(db: Session, account: models.Account) -> bool:
    """Cookie'lerle giriş yapılabiliyor mu? Durumu DB'ye işler."""
    try:
        client = _build_client(account)
        _run(_verify(client))
        account.status = "ok"
        account.last_error = ""
        account.last_checked = datetime.utcnow()
        db.commit()
        crud.add_log(db, f"Hesap doğrulandı: @{account.username or account.label}",
                     source="x_engine")
        return True
    except AccountSuspended as e:
        _mark_error(db, account, "suspended", f"Hesap askıya alınmış: {e}")
    except (Unauthorized, Forbidden) as e:
        _mark_error(db, account, "error", f"Cookie geçersiz/oturum düşmüş: {e}")
    except Exception as e:
        _mark_error(db, account, "error", f"Doğrulama hatası: {e}")
    return False


def _mark_error(db: Session, account: models.Account, status: str, msg: str):
    account.status = status
    account.last_error = msg[:500]
    account.last_checked = datetime.utcnow()
    if status == "suspended":
        account.is_active = False  # askıdaki hesap rotasyondan düşer
    db.commit()
    crud.add_log(db, f"[{account.label}] {msg}", level="error", source="x_engine")


# ------------------------------------------------------------------ tweet atma
def _download_media(url: str) -> bytes | None:
    """Görseli indirir (pbs.twimg.com). Başarısızsa None."""
    try:
        from curl_cffi import requests as creq
        r = creq.get(url, impersonate="chrome", timeout=25)
        if r.status_code == 200 and r.content:
            return r.content
    except Exception as e:
        log.warning("Görsel indirilemedi %s: %s", url[:80], e)
    return None


async def _create_tweet(client: Client, text: str, reply_to: str | None = None,
                        quote_url: str | None = None,
                        media_bytes: bytes | None = None) -> str:
    """Tweet atar ve tweet id döner.

    DİKKAT: `client.create_tweet()` KULLANILMAZ. O metot tweet'i gönderdikten
    SONRA yanıtı Tweet modeline çevirirken `KeyError: 'urls'` ile patlıyor
    (X kullanıcı JSON alanlarını değiştirdi). Sonuç: tweet X'e gidiyor ama
    bize hata dönüyor; üst katman "başarısız" sayıp TEKRAR gönderiyor ve
    mükerrer tweet oluşuyor.

    Bu yüzden alt seviye GraphQL çağrısı doğrudan yapılır ve id ham yanıttan
    okunur; twikit'in model ayrıştırması hiç devreye girmez.
    """
    media_entities = []
    if media_bytes:
        media_id = await client.upload_media(media_bytes,
                                             wait_for_completion=True)
        media_entities = [{"media_id": media_id, "tagged_users": []}]

    response, _ = await client.gql.create_tweet(
        False,          # is_note_tweet
        text,
        media_entities,
        None,           # poll_uri
        reply_to,
        quote_url,      # attachment_url -> alıntı tweet
        None,           # community_id
        False,          # share_with_followers
        None,           # richtext_options
        None,           # edit_tweet_id
        None,           # limit_mode
    )
    if "errors" in response:
        raise_exceptions_from_response(response["errors"])
        raise RuntimeError(
            response["errors"][0] if response["errors"] else "Tweet gönderilemedi"
        )
    result = (
        response.get("data", {})
        .get("create_tweet", {})
        .get("tweet_results", {})
        .get("result", {})
    )
    tweet_id = result.get("rest_id") or (result.get("legacy") or {}).get("id_str")
    if not tweet_id:
        # Gönderim büyük olasılıkla başarılı ama id okunamadı: tekrar
        # göndermemek için özel işaret döndür.
        log.warning("Tweet id okunamadı, yanıt: %s", str(response)[:300])
        return "unknown"
    return str(tweet_id)


def post_tweet(db: Session, account: models.Account, text: str,
               reply_to: str | None = None,
               quote_url: str | None = None,
               media_url: str | None = None) -> str | None:
    """Tweet atar; başarılıysa tweet id döner, değilse None.

    quote_url verilirse ALINTI TWEET olur: hedef tweet gömülü olarak görünür,
    kaynak hesap görünür kalır. Yayıncının gol videosunu kendi hesabımıza
    yüklemek telif ihlali olurdu; alıntı X'in kendi paylaşım mekanizmasıdır
    ve hiçbir içeriği çoğaltmaz.
    """
    try:
        media_bytes = None
        if media_url:
            media_bytes = _download_media(media_url)
            if media_bytes is None:
                # Görsel çekilemedi: görselsiz "skor kartı" metni havada
                # kalır — göndermeyi iptal etmek daha doğru.
                crud.add_log(db, "Görsel indirilemedi, gönderim atlandı",
                             level="warn", source="x_engine")
                return None
        client = _build_client(account)
        tweet_id = _run(_create_tweet(client, text, reply_to, quote_url,
                                      media_bytes))
        account.status = "ok"
        account.last_error = ""
        account.last_checked = datetime.utcnow()
        db.commit()
        return tweet_id
    except AccountSuspended as e:
        _mark_error(db, account, "suspended", f"Tweet atılamadı, hesap askıda: {e}")
    except (Unauthorized, Forbidden) as e:
        _mark_error(db, account, "error", f"Tweet atılamadı, oturum sorunu: {e}")
    except TooManyRequests as e:
        # rate limit: hesabı bozuk sayma, sadece logla — dispatcher sonra dener
        crud.add_log(db, f"[{account.label}] Rate limit: {e}", level="warn", source="x_engine")
    except DuplicateTweet as e:
        # aynı metin: hesap sağlıklı, bu postu düşür
        crud.add_log(db, f"[{account.label}] Yinelenen tweet atlandı", level="warn", source="x_engine")
        return "duplicate"
    except AccountLocked as e:
        _mark_error(db, account, "error", f"Hesap kilitli (doğrulama gerekli): {e}")
    except Exception as e:
        _mark_error(db, account, "error", f"Tweet hatası: {e}")
        mesaj = str(e)
        # OTOMATİK FREN: X'in "otomatik davranış" bayrağı (kod 226) gelirse
        # insan müdahalesi BEKLEMEDEN paylaşım durdurulur. 17.08'de bu bayrağı
        # yedik; devam etmek gerçek askıya götürebilir. Elle yeniden açılır.
        if "226" in mesaj or "might be automated" in mesaj:
            crud.set_setting(db, "posting_enabled", "false")
            db.commit()
            crud.add_log(db, "OTOMATİK FREN: kod 226 (otomatik davranış "
                             "bayrağı) — paylaşım kendiliğinden durduruldu",
                         level="error", source="x_engine")
        # GÜNLÜK LİMİT (kod 344): X'in kendi tweet kotası doldu. Ban değil,
        # normal sınır. Israr etmek anlamsız; gün sonuna kadar durdur.
        # Ertesi gün cleanup işi tekrar açar (bkz. maintenance).
        elif "344" in mesaj or "daily limit" in mesaj:
            crud.set_setting(db, "posting_enabled", "false")
            crud.set_setting(db, "daily_limit_hit", timeutil.tr_now().strftime("%Y-%m-%d"))
            db.commit()
            crud.add_log(db, "GÜNLÜK LİMİT (kod 344): X tweet kotası doldu — "
                             "paylaşım gün sonuna kadar durduruldu, yarın açılır",
                         level="warn", source="x_engine")
    return None


# ------------------------------------------------------------------ rotasyon
def todays_post_count(db: Session, account_id: int) -> int:
    start = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0)
    return (
        db.query(models.Post)
        .filter(
            models.Post.account_id == account_id,
            models.Post.status == "posted",
            models.Post.posted_at >= start,
        )
        .count()
    )


def pick_account(db: Session, daily_cap: int) -> models.Account | None:
    """Rotasyon: aktif + sağlıklı + günlük tavanı dolmamış hesaplardan
    bugün en az paylaşım yapanı seç (yükü dengeler)."""
    candidates = (
        db.query(models.Account)
        .filter(
            models.Account.is_active == True,  # noqa: E712
            models.Account.status.in_(["ok", "unknown"]),
        )
        .all()
    )
    best, best_count = None, None
    for acc in candidates:
        n = todays_post_count(db, acc.id)
        if n >= daily_cap:
            continue
        if best_count is None or n < best_count:
            best, best_count = acc, n
    return best
