"""
PowerTools Thailand — Daily Competitor Price & Stock Monitor
============================================================
แพ็กเกจ Standard (AICE Automation) — เฟส 2 ส่งมอบงานจริง

Flow:
  Cloud Scheduler (ทุกวัน 08:00 ICT)
    -> Cloud Function (ฟังก์ชัน `run` ด้านล่าง, HTTP trigger)
       -> โหลด SKU ของลูกค้าจาก Google Sheet (แท็บ ClientProducts)
       -> scrape ราคา/สต็อกของคู่แข่งแต่ละเว็บ (per-site try/except + tenacity retry)
       -> เทียบราคา คำนวณ PriceDiff / PriceChange% / StockChanged
       -> เขียนผลลงแท็บ Results (batch append)
       -> แจ้งเตือน Telegram เมื่อ: คู่แข่งถูกกว่าเรา >= PRICE_DROP_ALERT_PCT %,
          คู่แข่งกลับมามีของ (restock), หรือ scraper พัง (selector drift)

ออกแบบให้ "เว็บเดียวล่ม ไม่ทำให้ทั้งระบบพัง" และ "ความผิดปกติทุกอย่างถูกแจ้ง ไม่เงียบหาย"
แทนค่า CONFIG ด้วย env (ดู config.example.env) + แนบ service_account.json สำหรับ gspread
"""
from __future__ import annotations

import os
import json
import time
import logging
import datetime as dt
from dataclasses import dataclass
from zoneinfo import ZoneInfo

import requests
from bs4 import BeautifulSoup
from tenacity import (
    retry, stop_after_attempt, wait_exponential, retry_if_exception_type,
)
import gspread

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("competitor-monitor")
BKK = ZoneInfo("Asia/Bangkok")

# ---------- CONFIG (ย้ายไป env / Secret Manager ตอน prod — ดู config.example.env) ----------
SHEET_ID = os.environ.get("SHEET_ID", "PUT_SHEET_ID")
CLIENT_TAB = os.environ.get("CLIENT_TAB", "ClientProducts")   # คอลัมน์: sku | name | client_price
RESULT_TAB = os.environ.get("RESULT_TAB", "Results")
TG_TOKEN = os.environ.get("TG_TOKEN", "PUT_BOT_TOKEN")
TG_CHAT = os.environ.get("TG_CHAT", "PUT_CHAT_ID")
GSA_JSON = os.environ.get("GSA_JSON", "service_account.json")
PRICE_DROP_ALERT_PCT = float(os.environ.get("PRICE_DROP_ALERT_PCT", "10"))
REQUEST_DELAY_SEC = float(os.environ.get("REQUEST_DELAY_SEC", "1.5"))   # มารยาท + กัน IP โดนแบน
HTTP_TIMEOUT = int(os.environ.get("HTTP_TIMEOUT", "20"))

HEADERS = {
    "User-Agent": (
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
        "(KHTML, like Gecko) Chrome/124.0 Safari/537.36"
    ),
    "Accept-Language": "th,en;q=0.8",
}

# เป้าหมาย scrape: competitor -> {sku: {url, price_sel, stock_sel}}
# โหลดจากไฟล์ targets.json ถ้ามี (แก้รายการได้โดยไม่ต้องแตะโค้ด) ไม่งั้นใช้ตัวอย่างด้านล่าง
def _load_targets() -> dict:
    path = os.environ.get("TARGETS_JSON", "targets.json")
    if os.path.exists(path):
        with open(path, encoding="utf-8") as f:
            return json.load(f)
    return {
        "ToolKingTH": {
            "SKU-DRILL-01": {
                "url": "https://toolking.example/p/cordless-drill-x",
                "price_sel": "span.product-price",
                "stock_sel": "div.stock-status",
            },
        },
        "MegaHardware": {
            "SKU-DRILL-01": {
                "url": "https://megahardware.example/items/cordless-drill-x",
                "price_sel": "[data-testid='price']",
                "stock_sel": "[data-testid='availability']",
            },
        },
    }


TARGETS = _load_targets()


@dataclass
class Scraped:
    competitor: str
    sku: str
    price: float | None
    stock: str
    ok: bool
    status: str = "Success"      # Success | Selector Error | Timeout | Error
    note: str = ""


def _parse_price(text: str) -> float | None:
    """ดึงตัวเลขราคาออกจากข้อความ เช่น '฿1,290.00' -> 1290.0 (ยืดหยุ่นต่อสัญลักษณ์สกุลเงิน)."""
    cleaned = "".join(c for c in (text or "") if c.isdigit() or c == ".")
    # กันกรณีมีจุดหลายตัว (เช่น 1.290.00) -> เก็บจุดสุดท้ายเป็นทศนิยม
    if cleaned.count(".") > 1:
        whole, _, frac = cleaned.rpartition(".")
        cleaned = whole.replace(".", "") + "." + frac
    try:
        return float(cleaned) if cleaned not in ("", ".") else None
    except ValueError:
        return None


@retry(
    stop=stop_after_attempt(3),
    wait=wait_exponential(multiplier=1, min=2, max=10),
    retry=retry_if_exception_type(requests.RequestException),
    reraise=True,
)
def _fetch(url: str) -> str:
    r = requests.get(url, headers=HEADERS, timeout=HTTP_TIMEOUT)
    r.raise_for_status()
    return r.text


def scrape_one(competitor: str, sku: str, cfg: dict) -> Scraped:
    """scrape 1 SKU จาก 1 คู่แข่ง — ออกแบบให้ความล้มเหลว 'ไม่ลาม' ไปทั้ง run."""
    try:
        soup = BeautifulSoup(_fetch(cfg["url"]), "html.parser")
        price_el = soup.select_one(cfg["price_sel"])
        stock_el = soup.select_one(cfg.get("stock_sel", ""))
        if price_el is None:                       # selector drift = สาเหตุพังอันดับ 1
            return Scraped(competitor, sku, None, "?", False,
                           "Selector Error", "price selector missed (schema drift?)")
        price = _parse_price(price_el.get_text())
        stock = stock_el.get_text(strip=True) if stock_el else "unknown"
        if price is None:
            return Scraped(competitor, sku, None, stock, False,
                           "Selector Error", "price unparseable")
        return Scraped(competitor, sku, price, stock, True, "Success", "")
    except requests.Timeout:
        log.warning("timeout %s/%s", competitor, sku)
        return Scraped(competitor, sku, None, "?", False, "Timeout", "request timed out")
    except Exception as e:                         # เว็บเดียวล่ม ต้องไม่ฆ่าทั้ง run
        log.exception("scrape failed %s/%s", competitor, sku)
        return Scraped(competitor, sku, None, "?", False, "Error", f"{type(e).__name__}: {e}")


def send_alert(text: str) -> None:
    """ส่งแจ้งเตือน Telegram — ถ้าส่งไม่ได้ก็ไม่ crash (พึ่ง Cloud Logging แทน)."""
    try:
        requests.post(
            f"https://api.telegram.org/bot{TG_TOKEN}/sendMessage",
            json={"chat_id": TG_CHAT, "text": text, "parse_mode": "HTML"},
            timeout=15,
        )
    except requests.RequestException:
        log.error("alert send failed (relying on Cloud Logging): %s", text)


def _prev_stock_map(ws) -> dict:
    """อ่านสต็อกล่าสุดต่อ (competitor, sku) จาก Results เพื่อตรวจ 'restock'."""
    prev = {}
    try:
        for row in ws.get_all_records():           # เรียงตามเวลา แถวหลังทับแถวก่อน = ค่าล่าสุด
            prev[(row.get("Competitor"), str(row.get("SKU")))] = row.get("ScrapedStock")
    except Exception:
        log.warning("cannot read previous Results (first run?) — skip restock detection")
    return prev


def run(request=None):
    """Cloud Function entrypoint (HTTP). เรียกโดย Cloud Scheduler ทุกวัน 08:00 ICT."""
    gc = gspread.service_account(filename=GSA_JSON)
    sh = gc.open_by_key(SHEET_ID)
    client = {str(r["sku"]): r for r in sh.worksheet(CLIENT_TAB).get_all_records()}
    result_ws = sh.worksheet(RESULT_TAB)
    prev_stock = _prev_stock_map(result_ws)

    ts = dt.datetime.now(BKK).strftime("%Y-%m-%d %H:%M:%S")
    rows, price_alerts, restock_alerts, failures = [], [], [], []

    for competitor, skus in TARGETS.items():
        for sku, cfg in skus.items():
            s = scrape_one(competitor, sku, cfg)
            cp = float(client.get(sku, {}).get("client_price") or 0) or None
            diff = round(cp - s.price, 2) if (cp and s.price) else None
            diff_pct = round((cp - s.price) / cp * 100, 1) if (cp and s.price) else None
            prev = prev_stock.get((competitor, sku))
            stock_changed = bool(prev is not None and prev != s.stock)

            # Data model 10 คอลัมน์ (ตรงกับที่วางไว้เฟส 1)
            rows.append([ts, competitor, sku, s.price, s.stock, cp,
                         diff, diff_pct, "YES" if stock_changed else "NO", s.status])

            if not s.ok:
                failures.append(f"{competitor}/{sku}: {s.note}")
            else:
                if diff_pct is not None and diff_pct >= PRICE_DROP_ALERT_PCT:
                    price_alerts.append(
                        f"⚠️ {competitor} ตัด {sku} ถูกกว่าเรา {diff_pct}% "
                        f"(เขา ฿{s.price:,.0f} / เรา ฿{cp:,.0f})"
                    )
                if stock_changed and "out" in str(prev).lower() and "out" not in s.stock.lower():
                    restock_alerts.append(f"🔄 {competitor} เติมของ {sku} แล้ว ({s.stock})")

            time.sleep(REQUEST_DELAY_SEC)           # rate-limit ตัวเอง

    result_ws.append_rows(rows, value_input_option="USER_ENTERED")  # batch = ประหยัด API quota

    if price_alerts:
        send_alert("📉 <b>คู่แข่งตัดราคา</b>\n" + "\n".join(price_alerts))
    if restock_alerts:
        send_alert("📦 <b>คู่แข่งเติมของ</b>\n" + "\n".join(restock_alerts))
    if failures:                                    # ความพังถูกแจ้งเสมอ ไม่เงียบ
        send_alert(f"🛠️ <b>Scraper มีปัญหา {len(failures)} จุด</b>\n" + "\n".join(failures[:10]))

    summary = {
        "timestamp": ts,
        "scraped": len(rows),
        "price_alerts": len(price_alerts),
        "restock_alerts": len(restock_alerts),
        "failures": len(failures),
    }
    log.info("run complete %s", summary)
    return (json.dumps(summary, ensure_ascii=False), 200, {"Content-Type": "application/json"})


if __name__ == "__main__":
    # รันโลคัลเพื่อทดสอบ (ใช้ env เดียวกับ prod)
    print(run())
