"""Stateless tracking URL encoding and HTML link rewriting.

Encodes tracking metadata (track_id, webhook_url, custom metadata) directly
into tracking URLs using URL-safe base64 — no database lookup needed to
decode on open/click events.

Inspired by pytracking's stateless approach.
"""
from __future__ import unicode_literals

import base64
import hashlib
import hmac
import json
import logging
import os
import re
import threading
import time
from html.parser import HTMLParser
from urllib.parse import urljoin, urlparse

log = logging.getLogger(__name__)

# ── HMAC signing to prevent tampering ────────────────────────────────────
_SIGNING_KEY = os.environ.get(
    "SIGNALMARK_SIGNING_KEY",
    os.environ.get("SECRET_KEY", "signalmark-secret-change-me"),
).encode()

_MAX_URL_LENGTH = 2048  # RFC safe limit for most email clients


def _sign(data: bytes) -> str:
    """Return hex HMAC-SHA256 signature for data."""
    return hmac.new(_SIGNING_KEY, data, hashlib.sha256).hexdigest()[:16]


def encode_tracking_data(track_id, webhook_url="", metadata=None):
    """Encode tracking data into a URL-safe base64 token.

    The token structure is:
        {payload_json_b64}.{signature}

    The payload contains:
        - tid: track_id
        - wh:  webhook_url (optional)
        - m:   arbitrary metadata dict (optional)
    """
    payload: dict = {"tid": track_id}
    if webhook_url:
        payload["wh"] = webhook_url
    if metadata:
        payload["m"] = metadata
    # Use compact separators — no spaces — so base64 tokens are URL-safe
    payload_bytes = json.dumps(payload, separators=(",", ":")).encode("utf-8")
    token = base64.urlsafe_b64encode(payload_bytes).rstrip(b"=").decode()
    sig = _sign(payload_bytes)
    return f"{token}.{sig}"


def decode_tracking_data(token):
    """Decode a tracking token back to its payload dict.

    Returns None if the signature is invalid or the token is malformed.
    """
    parts = token.rsplit(".", 1)
    if len(parts) != 2:
        return None
    b64_part, sig = parts
    # Re-pad base64
    padding = 4 - len(b64_part) % 4
    if padding != 4:
        b64_part += "=" * padding
    try:
        payload_bytes = base64.urlsafe_b64decode(b64_part)
    except Exception:
        return None
    expected_sig = _sign(payload_bytes)
    if not hmac.compare_digest(sig, expected_sig):
        return None
    try:
        return json.loads(payload_bytes)
    except Exception:
        return None


def make_click_proxy_url(base_url, track_id, original_url, webhook_url="", metadata=None):
    """Build a click-tracking proxy URL.

    The original URL is encoded into the path segment so the redirect
    endpoint can decode it and 302 to the real destination.
    """
    token = encode_tracking_data(track_id, webhook_url, metadata)
    # Encode the original URL as base64 for the path (no signature needed —
    # the track_id in the token already authenticates the request).
    url_b64 = base64.urlsafe_b64encode(original_url.encode()).rstrip(b"=").decode()
    return f"{base_url.rstrip('/')}/api/click/{token}/{url_b64}"


def decode_click_url(path_token, url_b64):
    """Decode a click proxy URL.

    Returns (tracking_data, original_url) or (None, None) on failure.
    """
    data = decode_tracking_data(path_token)
    if not data:
        return None, None
    padding = 4 - len(url_b64) % 4
    if padding != 4:
        url_b64 += "=" * padding
    try:
        original_url = base64.urlsafe_b64decode(url_b64).decode()
    except Exception:
        return None, None
    # Basic validation
    parsed = urlparse(original_url)
    if parsed.scheme not in ("http", "https"):
        return None, None
    return data, original_url


# ── HTML link rewriting ──────────────────────────────────────────────────

def rewrite_html_links(html, base_click_url, track_id, webhook_url="", metadata=None):
    """Rewrite all <a href="..."> links in HTML to go through click proxy.

    Skips:
      - mailto: links
      - tel: links
      - javascript: links
      - anchor-only links (#...)
      - Links already pointing to the same tracking domain
    """
    click_domain = urlparse(base_click_url).netloc

    def _replace_link(match):
        prefix = match.group(1)  # e.g. 'href="' or "href='"
        url = match.group(2)
        suffix = match.group(3)  # closing quote

        # Skip non-HTTP links
        lower = url.lower().strip()
        if lower.startswith(("mailto:", "tel:", "javascript:", "#")):
            return match.group(0)

        # Resolve relative URLs
        full_url = urljoin("https://placeholder.com", url)
        parsed = urlparse(full_url)

        # Skip if already pointing to our tracking domain
        if parsed.netloc == click_domain:
            return match.group(0)

        # Skip tracking pixel and font URLs (these are our own endpoints)
        if "/api/tracks/" in url and any(
            url.endswith(s) for s in ("/open", "/open2", "/open3", "/font.css")
        ):
            return match.group(0)

        proxy_url = make_click_proxy_url(
            base_click_url, track_id, full_url, webhook_url, metadata
        )
        return f"{prefix}{proxy_url}{suffix}"

    # Match href="..." and href='...' — but not inside <style> or <script>
    pattern = r"""(href\s*=\s*)(["'])(https?://[^"']*?)\2"""
    return re.sub(pattern, _replace_link, html, flags=re.IGNORECASE)


class _HTMLLinkRewriter(HTMLParser):
    """SAX-style HTML parser that rewrites only <a href> attributes."""

    def __init__(self, base_click_url, track_id, webhook_url="", metadata=None):
        super(_HTMLLinkRewriter, self).__init__()
        self._base = base_click_url
        self._track_id = track_id
        self._wh = webhook_url
        self._meta = metadata
        self._click_domain = urlparse(base_click_url).netloc
        self.result = []
        self._skip_tag = None

    def handle_starttag(self, tag, attrs):
        if tag in ("script", "style"):
            self._skip_tag = tag
            # Reconstruct the opening tag as-is
            attr_str = " ".join(
                f'{k}="{v}"' if v is not None else k for k, v in attrs
            )
            self.result.append(f"<{tag} {attr_str}>" if attr_str else f"<{tag}>")
            return

        if tag == "a":
            new_attrs = []
            for k, v in attrs:
                if k.lower() == "href" and v:
                    lower = v.lower().strip()
                    if (lower.startswith(("mailto:", "tel:", "javascript:", "#"))
                            or self._click_domain in v
                            or ("/api/tracks/" in v and any(
                                v.endswith(s) for s in
                                ("/open", "/open2", "/open3", "/font.css")
                            ))):
                        new_attrs.append((k, v))
                    else:
                        proxy = make_click_proxy_url(
                            self._base, self._track_id, v,
                            self._wh, self._meta,
                        )
                        new_attrs.append((k, proxy))
                else:
                    new_attrs.append((k, v))
            attrs = new_attrs

        attr_str = " ".join(
            f'{k}="{v}"' if v is not None else k for k, v in attrs
        )
        self.result.append(f"<{tag} {attr_str}>" if attr_str else f"<{tag}>")

    def handle_endtag(self, tag):
        if self._skip_tag == tag:
            self._skip_tag = None
        self.result.append(f"</{tag}>")

    def handle_data(self, data):
        self.result.append(data)

    def handle_entityref(self, name):
        self.result.append(f"&{name};")

    def handle_charref(self, name):
        self.result.append(f"&#{name};")

    def handle_comment(self, data):
        self.result.append(f"<!--{data}-->")

    def handle_decl(self, decl):
        self.result.append(f"<!{decl}>")

    def handle_pi(self, data):
        self.result.append(f"<?{data}>")

    def unknown_decl(self, data):
        self.result.append(f"<![{data}]>")


def rewrite_html_links_safe(html, base_click_url, track_id, webhook_url="", metadata=None):
    """Rewrite links using SAX parser — safer for complex HTML."""
    rewriter = _HTMLLinkRewriter(
        base_click_url, track_id, webhook_url, metadata
    )
    try:
        rewriter.feed(html)
        return "".join(rewriter.result)
    except Exception:
        # Fall back to regex on parse failure
        log.warning("HTML parse failed, falling back to regex link rewriting")
        return rewrite_html_links(html, base_click_url, track_id,
                                  webhook_url, metadata)


# ── Outbound webhook firing ──────────────────────────────────────────────

def fire_webhook(webhook_url, event):
    """POST a tracking event to an external webhook URL.

    Runs in a background thread so it never blocks the request.
    If the webhook fails, the error is logged but not raised.
    """
    if not webhook_url:
        return

    def _send():
        try:
            import urllib.request
            req = urllib.request.Request(
                webhook_url,
                data=json.dumps(event, default=str).encode(),
                headers={"Content-Type": "application/json"},
                method="POST",
            )
            with urllib.request.urlopen(req, timeout=10) as resp:
                log.info("Webhook POST to %s → %d", webhook_url, resp.status)
        except Exception as exc:
            log.warning("Webhook POST to %s failed: %s", webhook_url, exc)

    threading.Thread(target=_send, daemon=True, name="webhook-fire").start()
