import asyncio, json, html, time, re
from js import document, window, console
from pyodide.http import pyfetch
from pyodide.ffi import create_proxy

API_BASE = "/chatapp/api"
DEBUG = True

# Globals (keep references so proxies don't get GC'd)
_send_click_proxy = None
_enter_proxy = None
_poll_task = None
_profile_save_proxy = None
_bookmark_proxy = None

_thread_id = 0
_buyer_token = ""
_last_send_at = 0.0
_page_loaded_at = time.monotonic()

# Tunables
SEND_COOLDOWN_SEC = 3.0
WARMUP_SEC = 1.2
MAX_LEN = 1000
MAX_LINKS = 2


def log_debug(*args):
    if not DEBUG:
        return
    try:
        console.log("[chat.py]", *args)
    except Exception:
        pass


def log_error(*args):
    try:
        console.error("[chat.py]", *args)
    except Exception:
        pass


def el(id_):
    return document.getElementById(id_)


def set_status(msg: str):
    st = el("statusText")
    if st:
        st.textContent = msg
    log_debug("status:", msg)


def set_composer_enabled(enabled: bool):
    inp = el("msgInput")
    btn = el("sendBtn")

    try:
        if inp:
            inp.disabled = not enabled
        if btn:
            btn.disabled = not enabled
    except Exception:
        pass


def safe_text(v) -> str:
    try:
        return str(v or "").strip()
    except Exception:
        return ""


def to_int(v, default: int = 0) -> int:
    try:
        if v is None:
            return default
        if isinstance(v, bool):
            return default
        s = str(v).strip()
        if s == "":
            return default
        return int(s)
    except Exception:
        return default


def get_cfg() -> dict:
    tag = el("chatapp-config")
    if not tag:
        log_debug("chatapp-config tag missing")
        return {}
    raw = tag.textContent or "{}"
    try:
        cfg = json.loads(raw)
        if not isinstance(cfg, dict):
            return {}
        return cfg
    except Exception as e:
        log_error("chatapp-config JSON error:", e, raw)
        return {}


def get_param(name: str) -> str:
    try:
        qs = window.location.search or ""
        params = window.URLSearchParams.new(qs)
        v = params.get(name)
        return v if v is not None else ""
    except Exception:
        return ""


def get_hash_param(name: str) -> str:
    try:
        h = window.location.hash or ""
        if h.startswith("#"):
            h = h[1:]
        params = window.URLSearchParams.new(h)
        v = params.get(name)
        return v if v is not None else ""
    except Exception:
        return ""


def set_hash_param(name: str, value: str):
    try:
        params = window.URLSearchParams.new((window.location.hash or "").lstrip("#"))
        params.set(name, value)
        window.location.hash = params.toString()
    except Exception:
        pass


def ls_get(key: str) -> str:
    try:
        v = window.localStorage.getItem(key)
        return v if v is not None else ""
    except Exception:
        return ""


def ls_set(key: str, val: str):
    try:
        window.localStorage.setItem(key, val)
    except Exception:
        pass


def token_ls_key(thread_id: int) -> str:
    return f"ts_chat_tok_{thread_id}"


def buyer_profile_email_key() -> str:
    return "ts_chat_guest_email"


def buyer_profile_phone_key() -> str:
    return "ts_chat_guest_phone"


def get_honeypot_value() -> str:
    try:
        hp = el("hp")
        if hp and hasattr(hp, "value"):
            return safe_text(hp.value)
    except Exception:
        pass
    return ""


def count_links(text: str) -> int:
    try:
        return len(re.findall(r"https?://", text, flags=re.IGNORECASE))
    except Exception:
        return 0


def is_valid_email(s: str) -> bool:
    s = (s or "").strip()
    if len(s) < 5 or "@" not in s:
        return False
    return bool(re.match(r"^[^@\s]+@[^@\s]+\.[^@\s]+$", s))


def normalize_phone(s: str) -> str:
    s = (s or "").strip()
    if not re.match(r"^[0-9+\-\s()]{7,30}$", s):
        return ""
    out = []
    for ch in s:
        if ch.isdigit() or ch == "+":
            out.append(ch)
    return "".join(out)[:20]


def inject_profile_gate():
    """
    Injects a modal that forces the visitor to enter email + phone.
    Keep the required IDs intact:
      - profileGate
      - buyerEmail
      - buyerPhone
      - profileErr
      - profileSaveBtn
    """
    if el("profileGate"):
        return

    overlay = document.createElement("div")
    overlay.id = "profileGate"
    overlay.setAttribute("role", "dialog")
    overlay.setAttribute("aria-modal", "true")
    overlay.setAttribute("aria-labelledby", "profileGateTitle")
    overlay.style.cssText = (
        "position:fixed;inset:0;z-index:9999;"
        "display:flex;align-items:center;justify-content:center;"
        "padding:18px;background:rgba(5,5,5,.84);"
        "backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);"
    )

    overlay.innerHTML = """
    <style>
      #profileGate,
      #profileGate *{
        box-sizing:border-box;
      }

      #profileGate .ts-profile-modal{
        width:min(100%, 520px);
        max-height:calc(100vh - 36px);
        overflow:auto;
        border:1px solid #1f1f1f;
        border-radius:20px;
        background:#0a0a0a;
        color:#f0f0f0;
        box-shadow:0 30px 60px -15px rgba(0,0,0,.80);
        font-family:'Inter',system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
      }

      #profileGate .ts-profile-modal::before{
        content:"";
        position:absolute;
        inset:0;
        pointer-events:none;
        background:radial-gradient(circle at top center, rgba(65,105,225,.12), transparent 66%);
        border-radius:20px;
      }

      #profileGate .ts-profile-modal-inner{
        position:relative;
        z-index:1;
        padding:2rem;
      }

      #profileGate .ts-profile-badge{
        width:max-content;
        max-width:100%;
        min-height:32px;
        display:inline-flex;
        align-items:center;
        justify-content:center;
        margin-bottom:1rem;
        padding:.45rem .85rem;
        border:1px solid #222;
        border-radius:999px;
        background:#111;
        color:#4169E1;
        font-size:.72rem;
        font-weight:900;
        letter-spacing:.14em;
        text-transform:uppercase;
      }

      #profileGate .ts-profile-title{
        margin:0;
        color:#4169E1;
        font-size:clamp(2rem, 7vw, 3rem);
        line-height:.95;
        font-weight:900;
        letter-spacing:-.06em;
        text-transform:uppercase;
      }

      #profileGate .ts-profile-copy{
        margin:1rem 0 1.25rem;
        color:#aaaaaa;
        font-size:.98rem;
        line-height:1.7;
      }

      #profileGate .ts-profile-grid{
        display:grid;
        gap:1rem;
      }

      #profileGate .ts-profile-field label{
        display:block;
        margin-bottom:.5rem;
        color:#aaaaaa;
        font-size:.92rem;
        font-weight:700;
      }

      #profileGate .ts-profile-input{
        width:100%;
        min-height:56px;
        padding:.9rem 1rem;
        border:1px solid #222;
        border-radius:12px;
        background:#111;
        color:#fff;
        outline:none;
        box-shadow:none;
        font-size:16px;
        font-weight:600;
        transition:border-color .2s ease, background-color .2s ease, box-shadow .2s ease;
      }

      #profileGate .ts-profile-input::placeholder{
        color:#555;
      }

      #profileGate .ts-profile-input:focus{
        border-color:#4169E1;
        background:#1a1a1a;
        box-shadow:0 0 0 4px rgba(65,105,225,.15);
      }

      #profileGate #profileErr{
        display:none;
        padding:.85rem 1rem;
        border:1px solid rgba(251,113,133,.35);
        border-radius:12px;
        background:rgba(251,113,133,.12);
        color:#fecdd3;
        font-size:.92rem;
        font-weight:700;
        line-height:1.5;
      }

      #profileGate .ts-profile-button{
        min-height:56px;
        width:100%;
        display:inline-flex;
        align-items:center;
        justify-content:center;
        border:0;
        border-radius:12px;
        background:#4169E1;
        color:#fff;
        font-size:1.02rem;
        font-weight:900;
        letter-spacing:.02em;
        cursor:pointer;
        transition:background-color .2s ease, transform .2s ease;
      }

      #profileGate .ts-profile-button:hover,
      #profileGate .ts-profile-button:focus{
        background:#5a7fe8;
        transform:translateY(-1px);
      }

      #profileGate .ts-profile-note{
        margin:0;
        padding:1rem;
        border:1px solid #222;
        border-radius:12px;
        background:#111;
        color:#777;
        font-size:.82rem;
        line-height:1.55;
      }

      @media (max-width:575.98px){
        #profileGate{
          align-items:flex-start !important;
          padding:12px !important;
          overflow:auto;
        }

        #profileGate .ts-profile-modal{
          width:100%;
          max-height:none;
          margin:0;
        }

        #profileGate .ts-profile-modal-inner{
          padding:1.35rem;
        }

        #profileGate .ts-profile-title{
          font-size:2rem;
        }
      }
    </style>

    <div class="ts-profile-modal">
      <div class="ts-profile-modal-inner">
        <div class="ts-profile-badge">SECURE_CONTACT_GATE</div>

        <h2 class="ts-profile-title" id="profileGateTitle">Contact Details</h2>

        <p class="ts-profile-copy">
          Please add your email and phone number so Tony can respond quickly and keep the conversation connected to the right inquiry.
        </p>

        <div class="ts-profile-grid">
          <div class="ts-profile-field">
            <label for="buyerEmail">Email</label>
            <input
              id="buyerEmail"
              class="ts-profile-input"
              type="email"
              autocomplete="email"
              inputmode="email"
              placeholder="you@example.com"
            >
          </div>

          <div class="ts-profile-field">
            <label for="buyerPhone">Phone</label>
            <input
              id="buyerPhone"
              class="ts-profile-input"
              type="tel"
              autocomplete="tel"
              inputmode="tel"
              placeholder="+1XXXXXXXXXX"
            >
          </div>

          <div id="profileErr"></div>

          <button id="profileSaveBtn" class="ts-profile-button" type="button">
            Continue to Chat
          </button>

          <p class="ts-profile-note">
            We may also record an approximate location from your IP, such as city, region, and country, to help reduce spam and protect the chat system.
          </p>
        </div>
      </div>
    </div>
    """

    document.body.appendChild(overlay)

    try:
        em = ls_get(buyer_profile_email_key())
        ph = ls_get(buyer_profile_phone_key())

        if em and el("buyerEmail"):
            el("buyerEmail").value = em
        if ph and el("buyerPhone"):
            el("buyerPhone").value = ph

        if el("buyerEmail") and not em:
            el("buyerEmail").focus()
        elif el("buyerPhone"):
            el("buyerPhone").focus()
    except Exception:
        pass


def hide_profile_gate():
    gate = el("profileGate")
    if gate and gate.parentNode:
        gate.parentNode.removeChild(gate)


async def api_post(path: str, payload: dict) -> dict:
    url = f"{API_BASE}/{path.lstrip('/')}"
    try:
        log_debug("POST", url, "payload keys:", list(payload.keys()))

        res = await pyfetch(
            url,
            method="POST",
            headers={
                "Content-Type": "application/json",
                "Accept": "application/json",
            },
            body=json.dumps(payload),
            credentials="same-origin",
        )

        txt = await res.string()
        log_debug("response", url, "status=", res.status, "body=", txt[:250])

        try:
            data = json.loads(txt)
        except Exception:
            return {
                "ok": False,
                "error": f"Non-JSON response ({res.status}) from {url}: {txt[:250]}"
            }

        if not isinstance(data, dict):
            return {"ok": False, "error": f"Bad API response from {url}"}

        if "ok" not in data and not getattr(res, "ok", False):
            return {
                "ok": False,
                "error": f"HTTP {res.status} from {url}"
            }

        return data
    except Exception as e:
        log_error("api_post error:", url, e)
        return {"ok": False, "error": f"{url}: {e}"}


def render_messages(items: list):
    log = el("chatLog")
    if not log:
        log_debug("chatLog element missing")
        return

    parts = []
    for m in items or []:
        side = safe_text(m.get("side", ""))
        who = safe_text(m.get("sender_label", ""))
        body = safe_text(m.get("body", ""))
        ts = safe_text(m.get("created_at", ""))

        who_html = html.escape(who or side or "User")
        body_html = html.escape(body)
        ts_html = html.escape(ts)

        parts.append(
            f"""
            <div class="msg">
              <div class="who">
                <span class="side">{html.escape(side)}</span>
                <div style="font-weight:700">{who_html}</div>
                <div style="font-size:.82rem;opacity:.7">{ts_html}</div>
              </div>
              <div class="body">{body_html}</div>
            </div>
            """
        )

    log.innerHTML = "".join(parts) if parts else '<div class="small-muted">No messages yet.</div>'
    try:
        log.scrollTop = log.scrollHeight
    except Exception:
        pass


async def load_messages():
    global _thread_id, _buyer_token

    if _thread_id <= 0:
        log_debug("load_messages skipped: no thread_id")
        return

    cfg = get_cfg()
    payload = {"thread_id": _thread_id}
    if _buyer_token:
        payload["token"] = _buyer_token
    if cfg.get("csrf"):
        payload["csrf"] = cfg.get("csrf")

    out = await api_post("chat/get_messages.php", payload)
    if not out.get("ok"):
        set_status(out.get("error", "Failed to load messages"))
        return

    render_messages(out.get("messages", []))
    set_status("Ready")


async def ensure_profile_required():
    """
    Forces visitor to enter email + phone, saved in localStorage.
    """
    saved_email = safe_text(ls_get(buyer_profile_email_key()))
    saved_phone = safe_text(ls_get(buyer_profile_phone_key()))
    if saved_email and saved_phone:
        log_debug("profile already present in localStorage")
        return saved_email, saved_phone

    inject_profile_gate()
    log_debug("profile gate shown")

    fut = asyncio.get_event_loop().create_future()

    def on_save(evt):
        try:
            email = safe_text(el("buyerEmail").value if el("buyerEmail") else "")
            phone_raw = safe_text(el("buyerPhone").value if el("buyerPhone") else "")
            phone = normalize_phone(phone_raw)

            err = ""
            if not is_valid_email(email):
                err = "Please enter a valid email."
            elif phone == "":
                err = "Please enter a valid phone number."

            pe = el("profileErr")
            if err:
                if pe:
                    pe.style.display = "block"
                    pe.textContent = err
                log_debug("profile validation failed")
                return

            if pe:
                pe.style.display = "none"
                pe.textContent = ""

            ls_set(buyer_profile_email_key(), email)
            ls_set(buyer_profile_phone_key(), phone)
            hide_profile_gate()
            log_debug("profile saved; continuing to thread create")

            if not fut.done():
                fut.set_result((email, phone))
        except Exception as e:
            log_error("profile save error:", e)

    global _profile_save_proxy
    _profile_save_proxy = create_proxy(on_save)
    btn = el("profileSaveBtn")
    if btn:
        btn.addEventListener("click", _profile_save_proxy)

    return await fut


def ensure_bookmark_button():
    """
    Adds a 'Bookmark this chat' button if one doesn't exist.
    """
    if el("bookmarkChatBtn"):
        return

    btn = document.createElement("button")
    btn.id = "bookmarkChatBtn"
    btn.type = "button"
    btn.textContent = "Bookmark this chat"
    btn.style.cssText = (
        "position:fixed;right:14px;bottom:14px;z-index:9998;"
        "min-height:46px;border:1px solid #4169E1;border-radius:999px;"
        "padding:12px 15px;font-weight:900;letter-spacing:.02em;cursor:pointer;"
        "color:#fff;background:#4169E1;"
        "box-shadow:0 18px 42px rgba(0,0,0,.55);"
        "font-family:'Inter',system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;"
    )
    document.body.appendChild(btn)

    def on_click(evt):
        try:
            if _thread_id <= 0 or not _buyer_token:
                set_status("Chat not ready to bookmark yet.")
                return

            link = f"{window.location.origin}/chatapp/public/chat.php?thread_id={_thread_id}#t={_buyer_token}"

            try:
                window.navigator.clipboard.writeText(link)
                set_status("Bookmark link copied.")
            except Exception:
                window.prompt("Copy this link:", link)
        except Exception as e:
            log_error("bookmark click error:", e)

    global _bookmark_proxy
    _bookmark_proxy = create_proxy(on_click)
    btn.addEventListener("click", _bookmark_proxy)


def validate_message(body: str) -> str:
    body = (body or "").strip()

    if body == "":
        return "Message is empty."

    if (time.monotonic() - _page_loaded_at) < WARMUP_SEC:
        return "Please wait a moment before sending."

    if len(body) > MAX_LEN:
        return f"Message too long (max {MAX_LEN} characters)."

    if count_links(body) > MAX_LINKS:
        return f"Too many links (max {MAX_LINKS})."

    return ""


async def ensure_thread_or_resume():
    """
    Resume flow:
      /chatapp/public/chat.php?thread_id=123#t=TOKEN

    New flow:
      /chatapp/public/chat.php?listing_id=..&member_id=..&title=..
    """
    global _thread_id, _buyer_token

    set_composer_enabled(False)
    cfg = get_cfg()

    tid = to_int(get_param("thread_id"), 0)
    if tid > 0:
        _thread_id = tid

        tok = safe_text(get_hash_param("t"))
        if not tok:
            tok = safe_text(ls_get(token_ls_key(tid)))

        _buyer_token = tok

        log_debug("resume mode", {"thread_id": _thread_id, "has_token": bool(_buyer_token)})

        ensure_bookmark_button()
        set_status("Loading chat…")
        await load_messages()
        set_composer_enabled(True)
        return

    listing_id = to_int(cfg.get("listing_id"), 0)
    if listing_id <= 0:
        listing_id = to_int(get_param("listing_id"), 0)

    member_id = to_int(cfg.get("member_id"), 0)
    if member_id <= 0:
        member_id = to_int(get_param("member_id"), 0)

    title = safe_text(cfg.get("title")) or safe_text(get_param("title")) or "Contact"
    subject = safe_text(cfg.get("subject")) or title

    log_debug("new-thread context", {
        "listing_id": listing_id,
        "member_id": member_id,
        "subject": subject,
    })

    if listing_id <= 0 or member_id <= 0:
        set_status("Missing contact context or member_id.")
        set_composer_enabled(False)
        return

    email, phone = await ensure_profile_required()

    payload = {
        "listing_id": listing_id,
        "member_id": member_id,
        "subject": subject,
        "buyer_name": "Buyer",
        "buyer_email": email,
        "buyer_phone": phone,
        "hp": get_honeypot_value(),
    }

    if cfg.get("csrf"):
        payload["csrf"] = cfg.get("csrf")

    set_status("Opening chat…")
    out = await api_post("chat/create_thread.php", payload)
    if not out.get("ok"):
        set_status(out.get("error", "Failed to open thread"))
        set_composer_enabled(False)
        return

    _thread_id = to_int(out.get("thread_id"), 0)
    _buyer_token = safe_text(out.get("token", ""))

    log_debug("create_thread result", {
        "thread_id": _thread_id,
        "has_token": bool(_buyer_token),
        "resumed": bool(out.get("resumed")),
    })

    if _thread_id <= 0:
        set_status("Thread create returned no thread_id.")
        set_composer_enabled(False)
        return

    if _buyer_token:
        ls_set(token_ls_key(_thread_id), _buyer_token)
        set_hash_param("t", _buyer_token)

    try:
        url = window.URL.new(window.location.href)
        url.searchParams.set("thread_id", str(_thread_id))
        window.history.replaceState({}, "", url.toString())
    except Exception:
        pass

    ensure_bookmark_button()

    if el("threadTitle"):
        el("threadTitle").textContent = subject

    await load_messages()
    set_composer_enabled(True)


async def _cooldown_disable(btn, seconds: float):
    try:
        btn.disabled = True
    except Exception:
        return

    end = time.monotonic() + max(0.0, seconds)
    while True:
        remain = end - time.monotonic()
        if remain <= 0:
            break
        set_status(f"Please wait {remain:.1f}s…")
        await asyncio.sleep(0.2)

    try:
        btn.disabled = False
    except Exception:
        pass

    set_status("Ready")


async def send_current():
    global _thread_id, _buyer_token, _last_send_at

    if _thread_id <= 0:
        set_status("Thread not ready yet.")
        return

    inp = el("msgInput")
    btn = el("sendBtn")
    if not inp or not btn:
        log_debug("send_current aborted: missing msgInput or sendBtn")
        return

    body = safe_text(inp.value)

    now = time.monotonic()
    since = now - _last_send_at
    if since < SEND_COOLDOWN_SEC:
        asyncio.create_task(_cooldown_disable(btn, SEND_COOLDOWN_SEC - since))
        return

    err = validate_message(body)
    if err:
        set_status(err)
        return

    btn.disabled = True
    set_status("Sending…")

    cfg = get_cfg()
    payload = {"thread_id": _thread_id, "body": body, "hp": get_honeypot_value()}
    if _buyer_token:
        payload["token"] = _buyer_token
    if cfg.get("csrf"):
        payload["csrf"] = cfg.get("csrf")

    log_debug("sending message", {
        "thread_id": _thread_id,
        "body_len": len(body),
        "has_token": bool(_buyer_token),
    })

    out = await api_post("chat/send_message.php", payload)
    if not out.get("ok"):
        set_status(out.get("error", "Send failed"))
        try:
            btn.disabled = False
        except Exception:
            pass
        return

    inp.value = ""
    _last_send_at = time.monotonic()

    asyncio.create_task(_cooldown_disable(btn, SEND_COOLDOWN_SEC))
    await load_messages()


async def poll_loop():
    while True:
        await asyncio.sleep(3)
        try:
            await load_messages()
        except Exception as e:
            log_error("poll_loop error:", e)


def wire_events():
    global _send_click_proxy, _enter_proxy

    btn = el("sendBtn")
    inp = el("msgInput")
    if not btn or not inp:
        set_status("UI elements missing (sendBtn/msgInput).")
        return

    def on_click(evt):
        asyncio.create_task(send_current())

    def on_key(evt):
        try:
            if evt.key == "Enter":
                evt.preventDefault()
                asyncio.create_task(send_current())
        except Exception:
            pass

    _send_click_proxy = create_proxy(on_click)
    _enter_proxy = create_proxy(on_key)

    btn.addEventListener("click", _send_click_proxy)
    inp.addEventListener("keydown", _enter_proxy)
    log_debug("chat UI events wired")


async def main():
    try:
        log_debug("main start", window.location.href)
        set_composer_enabled(False)
        wire_events()
        await ensure_thread_or_resume()

        global _poll_task
        _poll_task = asyncio.create_task(poll_loop())
        log_debug("poll loop started")
    except Exception as e:
        log_error("chat.py main error:", e)
        set_status(f"Chat error: {e}")


asyncio.create_task(main())