# /var/www/html/bot/app/web/deps.py
from __future__ import annotations

import logging
import json
import re
import calendar
import time
from datetime import datetime, date, timedelta
from pathlib import Path
from typing import Optional, List, Dict, Any, Tuple
import hashlib

import requests
from starlette.templating import Jinja2Templates
from datetime import datetime, timezone, timedelta
from app.services.supabase_service import get_client

logger = logging.getLogger("govbot.web")

# ---------- 템플릿 루트 ----------
def _find_templates_dir() -> Path:
    here = Path(__file__).resolve()
    candidates = [
        here.parents[2] / "templates",  # /var/www/html/bot/templates
        here.parents[1] / "templates",
        here.parents[0] / "templates",
    ]
    for d in candidates:
        if d.exists():
            return d
    return candidates[0]

TEMPLATE_DIR = _find_templates_dir()
templates = Jinja2Templates(directory=str(TEMPLATE_DIR))

# ---------- Jinja filters (KST formatting) ----------
_KST = timezone(timedelta(hours=9))

def _to_kst(dt: datetime) -> datetime:
    if dt.tzinfo is None:
        dt = dt.replace(tzinfo=timezone.utc)
    return dt.astimezone(_KST)

def jinja_kst(value, fmt: str = "%Y-%m-%d %H:%M:%S") -> str:
    """Format ISO string/datetime to KST. Usage: {{ ts|kst }} or {{ ts|kst('%Y-%m-%d') }}"""
    if value in (None, ""):
        return "-"
    try:
        if isinstance(value, str):
            s = value.replace("Z", "+00:00")
            dt = datetime.fromisoformat(s)
        elif isinstance(value, datetime):
            dt = value
        else:
            # try string cast
            dt = datetime.fromisoformat(str(value).replace("Z", "+00:00"))
        return _to_kst(dt).strftime(fmt)
    except Exception:
        try:
            # fallback: only date part
            s = str(value)
            if len(s) >= 10:
                return s[:10]
        except Exception:
            pass
        return str(value)

# register filter
templates.env.filters["kst"] = jinja_kst

def jinja_election_label(value) -> str:
    try:
        n = int(value)
    except Exception:
        return "-"
    if n == 1:
        return "초선"
    if n == 2:
        return "재선"
    return f"{n}선"

templates.env.filters["election_label"] = jinja_election_label

# ---------- Supabase ----------
sb = get_client()

# ---------- 상수 ----------
COMPANY_ORDER = [
    "한국전력공사","한국남동발전","한국중부발전","한국서부발전","한국남부발전",
    "한국동서발전","한국수력원자력","한전KPS","한전KDN","한국전력기술","한전원자력연료",
]
POS_ORDER = ["상임기관장", "상임감사", "상임이사", "비상임이사"]
TODAY = date.today()

# ---------- 날짜/문자 유틸 ----------
_DATE_PATTERNS: List[Tuple[re.Pattern, str]] = [
    (re.compile(r"^\s*(\d{4})[.\-/년\s]*(\d{1,2})[.\-/월\s]*(\d{1,2})[일\s]*\s*$"), "ymd"),
    (re.compile(r"^\s*(\d{4})[.\-/년\s]*(\d{1,2})[.\-/월\s]*\s*$"), "ym"),
    (re.compile(r"^\s*(\d{4})\s*$"), "y"),
]

def _as_list(val) -> List[str]:
    if val is None:
        return []
    if isinstance(val, list):
        return [str(x).strip() for x in val if str(x).strip()]
    if isinstance(val, str):
        s = val.strip()
        if not s or s.lower() == "none":
            return []
        if s.startswith("[") and s.endswith("]"):
            try:
                v = json.loads(s)
                if isinstance(v, list):
                    return [str(x).strip() for x in v if str(x).strip()]
            except Exception:
                pass
        if "\n" in s:
            return [t.strip() for t in s.splitlines() if t.strip()]
        if "|" in s:
            return [t.strip() for t in s.split("|") if t.strip()]
        return [s]
    return [str(val).strip()]

def _fmt_date_display(y: Optional[int], m: Optional[int], d: Optional[int]) -> str:
    if y is None:
        return "-"
    mm = f"{m:02d}" if m else "??"
    dd = f"{d:02d}" if d else "??"
    return f"{y}-{mm}-{dd}"

def _parse_date_parts(s: str | None) -> Tuple[Optional[int], Optional[int], Optional[int]]:
    if not s:
        return None, None, None
    t = str(s).strip()
    for pat, kind in _DATE_PATTERNS:
        m = pat.match(t)
        if m:
            y = int(m.group(1))
            if kind == "ymd":
                return y, int(m.group(2)), int(m.group(3))
            if kind == "ym":
                return y, int(m.group(2)), None
            if kind == "y":
                return y, None, None
    try:
        d = datetime.fromisoformat(t[:10]).date()
        return d.year, d.month, d.day
    except Exception:
        return None, None, None

def _parts_to_date(y: Optional[int], m: Optional[int], d: Optional[int], assume_last: bool) -> Optional[date]:
    if y is None:
        return None
    if m is None:
        m = 12 if assume_last else 1
    if d is None:
        d = calendar.monthrange(y, m)[1] if assume_last else 1
    try:
        return date(y, m, d)
    except Exception:
        return None

def _date_display_and_compare(raw: Any) -> tuple[str, Optional[date]]:
    if raw is None:
        return "-", None
    s = str(raw).strip()
    if not s or s.lower() == "none":
        return "-", None
    y, m, d = _parse_date_parts(s)
    disp = _fmt_date_display(y, m, d)
    cmp_d = _parts_to_date(y, m, d, assume_last=True)
    return disp, cmp_d

def _to_int_safe(v) -> int:
    try:
        return int(v)
    except Exception:
        return 10**12

def _normalize_multiline(val) -> str:
    arr = _as_list(val)
    return "\n".join(arr) if arr else "-"

# ---------- 텍스트 정규화 ----------
def _normalize_name(s: str) -> str:
    return re.sub(r"\s+", "", (s or "")).upper()

def _norm_company_label(dep: str | None) -> str | None:
    if not dep:
        return None
    s = dep.strip()
    s0 = re.sub(r"(?:\(주\)|㈜|주식회사|\s+)", "", s, flags=re.I)
    sU = s0.upper()
    for lbl in COMPANY_ORDER:
        if lbl.replace(" ", "") in s0:
            return lbl
    if "KPS" in sU:
        return "한전KPS"
    if "KDN" in sU:
        return "한전KDN"
    if "KEPCO" in sU or "한국전력" in s0 or "한전" in s0:
        return "한국전력공사"
    if "남동발전" in s0:
        return "한국남동발전"
    if "중부발전" in s0:
        return "한국중부발전"
    if "서부발전" in s0:
        return "한국서부발전"
    if "남부발전" in s0:
        return "한국남부발전"
    if "동서발전" in s0:
        return "한국동서발전"
    if "수력원자력" in s0:
        return "한국수력원자력"
    if "전력기술" in s0:
        return "한국전력기술"
    if "원자력연료" in s0:
        return "한전원자력연료"
    return None

# ---------- 상태/집계 ----------
def _ping_status(url: str, timeout: float = 4.0) -> dict:
    try:
        headers = {
            # 일부 정부 사이트가 비브라우저 UA 차단 → 브라우저 UA 사용
            "User-Agent": ("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
                           "(KHTML, like Gecko) Chrome/119.0 Safari/537.36"),
            "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
        }
        t0 = time.time()
        # 1) 가벼운 HEAD 시도(리다이렉트 허용)
        r = requests.head(url, headers=headers, timeout=timeout, allow_redirects=True)
        # 일부 서버가 HEAD 403/405 응답 → 2) 얕은 GET 폴백
        if r.status_code in (403, 405):
            r = requests.get(url, headers=headers, timeout=timeout, stream=True, allow_redirects=True)
            r.close()  # 바디는 받지 않음
        ms = int((time.time() - t0) * 1000)
        ok = 200 <= r.status_code < 400
        label = "OK" if ok and ms < 1500 else ("지연" if ok else "불량")
        return {"label": label, "ms": ms, "status": r.status_code}
    except requests.exceptions.ConnectTimeout:
        return {"label": "불량", "ms": None, "error": "TIMEOUT"}
    except requests.exceptions.SSLError:
        return {"label": "불량", "ms": None, "error": "SSL"}
    except requests.exceptions.ProxyError:
        return {"label": "불량", "ms": None, "error": "PROXY"}
    except requests.exceptions.ConnectionError:
        return {"label": "불량", "ms": None, "error": "CONNECT"}
    except Exception as e:
        return {"label": "불량", "ms": None, "error": type(e).__name__}

def _count_last_24h(table: str) -> int:
    cutoff = datetime.utcnow() - timedelta(hours=24)
    try:
        res = sb.table(table).select("id,created_at").gte("created_at", cutoff.isoformat()).execute()
        return len(res.data or [])
    except Exception:
        try:
            res = sb.table(table).select("*").order("id", desc=True).limit(1000).execute()
            data = res.data or []
        except Exception:
            return 0
        cnt = 0
        for r in data:
            ts = r.get("created_at") or r.get("posted_at")
            if not ts:
                continue
            dt = None
            s = str(ts).strip()
            try:
                dt = datetime.fromisoformat(s[:19])
            except Exception:
                try:
                    dt = datetime.strptime(s[:10], "%Y-%m-%d")
                except Exception:
                    dt = None
            if dt and dt >= cutoff:
                cnt += 1
        return cnt

# ---------- MOTIE: person key utilities ----------
_COMMON_PHONES_DEFAULT = {
    # 대표/공용 번호들(빈번히 등장)
    "0442030000", "0552942774", "0552948018", "0438700000",
    "0614640745", "0635454812", "0634640709", "0617279793", "0335226182",
}

def norm_phone(phone: Optional[str]) -> Optional[str]:
    if phone is None:
        return None
    s = re.sub(r"\D", "", str(phone))
    return s or None

def is_common_phone(phone_norm: Optional[str]) -> bool:
    if not phone_norm:
        return False
    if phone_norm in _COMMON_PHONES_DEFAULT:
        return True
    if len(phone_norm) >= 4 and phone_norm.endswith("0000"):
        return True
    return False

def _sha1_hex(s: str) -> str:
    return hashlib.sha1(s.encode("utf-8")).hexdigest()

def _canon_motie_department(dep: Optional[str]) -> str:
    s = (dep or "").strip()
    if not s:
        return ""
    # basic whitespace normalization
    s = re.sub(r"\s+", " ", s)
    # known variants → canonical label (align with heads view)
    s = s.replace("제 1차관", "제1차관").replace("제 2차관", "제2차관")
    s = s.replace("제 1차관실", "제1차관실").replace("제 2차관실", "제2차관실")
    CANON = {
        "제1차관실": "제1차관",
        "제2차관실": "제2차관",
        "대변인실":  "대변인",
    }
    return CANON.get(s, s)

def motie_person_keys(
    name: Optional[str],
    phone: Optional[str],
    department: Optional[str] = None,
    position: Optional[str] = None,
) -> Dict[str, Optional[str]]:
    """Return both status key and display key for MOTIE staff.
    - status: sha1(name|phone_norm)
    - display: if phone is common → sha1(name|department|position), else status
    """
    nm = (name or "").strip()
    pn = norm_phone(phone) or ""
    status = _sha1_hex(f"{nm}|{pn}")
    disp = status
    if is_common_phone(pn):
        dep = _canon_motie_department(department)
        pos = (position or "").strip()
        disp = _sha1_hex(f"{nm}|{dep}|{pos}")
    return {"status": status, "display": disp}

def motie_status_key_raw(name: Optional[str], phone: Optional[str]) -> str:
    nm = (name or "").strip()
    ph = (phone or "").strip()
    return _sha1_hex(f"{nm}|{ph}")

def _today_items_from(
    table: str,
    source_label: str,
    title_key: str = "title",
    url_key: str = "url",
    tag_key: Optional[str] = "tag",
    limit_scan: int = 150,
) -> List[Dict[str, Any]]:
    today_str = date.today().isoformat()
    try:
        # id, bbsId, postId 등 URL 생성에 필요한 컬럼도 포함되도록 select("*")
        res = sb.table(table).select("*").order("id", desc=True).limit(limit_scan).execute()
        rows = res.data or []
    except Exception:
        rows = []

    out: List[Dict[str, Any]] = []
    for r in rows:
        d = str(r.get("posted_at") or r.get("created_at") or "")[:10]
        if d != today_str:
            continue

        # 기본 url
        url_val = r.get(url_key)

        # 폴백 URL 생성
        if not url_val:
            if source_label == "MOEF":
                url_val = _ensure_moef_url(r)
            elif source_label == "MOTIE":
                url_val = _ensure_motie_url(r)

        out.append(
            {
                "src": source_label,
                "title": r.get(title_key) or "-",
                "tag": (r.get(tag_key) if tag_key else None),
                "url": url_val,  # <- 항상 가능하면 채워져 나오게
                "date": d,
            }
        )
    return out
    
# app/web/deps.py (적당한 위치에 추가)

def _ensure_moef_url(row: dict) -> str | None:
    DETAIL_URL = "https://www.moef.go.kr/nw/notice/hrDetail.do"
    menuNo = "4050300"
    bbsId = row.get("bbsId"); postId = row.get("postId")
    if not (bbsId and postId):
        rid = str(row.get("id") or "")
        if "-" in rid:
            parts = rid.split("-", 1)
            if len(parts) == 2:
                bbsId, postId = parts
    if bbsId and postId:
        return f"{DETAIL_URL}?searchBbsId1={bbsId}&searchNttId1={postId}&menuNo={menuNo}"
    return None

def _ensure_motie_url(row: dict) -> str | None:
    BASE = "https://www.motie.go.kr"
    aid = row.get("id")
    if aid is None:
        return None
    return f"{BASE}/kor/article/ATCL6e90bb9de/{aid}/view?"


def _current_map_by_person_source():
    rows = sb.table("gov_staff_current").select("*").execute().data or []
    return {(r["person_id"], r["source"]): r for r in rows}

def _normalize_name(s: str) -> str:
    return re.sub(r"\s+", "", (s or "")).upper()

# ---------- 화면 정렬 상수 재수출 ----------
# (중요) 다른 페이지에서 그대로 deps를 임포트할 수 있도록 재수출
try:
    from .ordering import (
        _MOTIE_ORDER_POS,
        DEPT_ORDER, EXPECTED_POS, PHONE_ROLE_LAST4,
        MOEF_ORDER_POS,
        MOEF_KPS_DEPT_ORDER, MOEF_KPS_DEPT_WHITELIST, MOEF_KPS_EXPECTED_POS, MOEF_KPS_PHONE_ROLE_LAST4,
    )
except Exception:
    # ordering.py가 없더라도 deps 자체는 동작하도록
    _MOTIE_ORDER_POS = []            # type: ignore
    DEPT_ORDER = []                  # type: ignore
    EXPECTED_POS = {}                # type: ignore
    PHONE_ROLE_LAST4 = []            # type: ignore
    MOEF_ORDER_POS = []              # type: ignore
    MOEF_KPS_DEPT_ORDER = []         # type: ignore
    MOEF_KPS_DEPT_WHITELIST = []     # type: ignore
    MOEF_KPS_EXPECTED_POS = {}       # type: ignore
    MOEF_KPS_PHONE_ROLE_LAST4 = []   # type: ignore

__all__ = [
    # templates/sb/logger
    "templates", "sb", "logger", "TEMPLATE_DIR",
    # constants
    "COMPANY_ORDER", "POS_ORDER", "TODAY",
    # utils
    "_today_items_from", "_ping_status", "_count_last_24h",
    "_date_display_and_compare", "_to_int_safe", "_normalize_multiline",
    "_norm_company_label", "_parse_date_parts", "_parts_to_date", "_fmt_date_display",
    "_normalize_name",
    # re-exported ordering
    "_MOTIE_ORDER_POS",
    "DEPT_ORDER", "EXPECTED_POS", "PHONE_ROLE_LAST4",
    "MOEF_ORDER_POS",
    "MOEF_KPS_DEPT_ORDER", "MOEF_KPS_DEPT_WHITELIST", "MOEF_KPS_EXPECTED_POS", "MOEF_KPS_PHONE_ROLE_LAST4",
    "_current_map_by_person_source", "_normalize_name",
]
