"""Python 3.10+: bounded polling for a message sent by your own test system."""
import argparse
import json
import os
import time
import urllib.error
import urllib.parse
import urllib.request
from email.utils import parseaddr, parsedate_to_datetime


class NoRedirect(urllib.request.HTTPRedirectHandler):
    def redirect_request(self, req, fp, code, msg, headers, newurl):
        return None  # Never forward an API key to a redirect destination.


def request_inbox(address, key, timeout):
    query = urllib.parse.urlencode({"email": address, "summary": "1"})
    request = urllib.request.Request(
        "https://cleantempmail.com/api/emails?" + query,
        headers={"X-API-Key": key, "Accept": "application/json"},
    )
    try:
        with urllib.request.build_opener(NoRedirect).open(request, timeout=timeout) as response:
            return response.status, response.headers, response.read()
    except urllib.error.HTTPError as error:
        with error:
            return error.code, error.headers, error.read(65536)  # Inspect, never log error bodies.


def retry_delay(value):
    try:
        return max(0, float(value))
    except (TypeError, ValueError):
        try:
            return max(0, parsedate_to_datetime(value).timestamp() - time.time())
        except (TypeError, ValueError, OverflowError):
            return 5


def wait_for_email(address, key, sender, subject_marker, *, timeout=90,
                   max_attempts=18, interval=5, since=None, request=request_inbox,
                   clock=time.monotonic, sleep=time.sleep):
    if not address or not key or not sender or not subject_marker:
        raise ValueError("Address, API key, expected sender and a unique subject marker are required")
    if timeout <= 0 or max_attempts < 1 or interval <= 0:
        raise ValueError("Polling limits must be positive")
    since = int(time.time()) if since is None else since
    deadline = clock() + timeout
    for attempt in range(max_attempts):
        remaining = deadline - clock()
        if remaining <= 0:
            break
        delay = interval
        try:
            status, headers, raw = request(address, key, min(10, remaining))
        except (TimeoutError, urllib.error.URLError):
            status, headers, raw = 503, {}, b""
        if status == 200:
            try:
                payload = json.loads(raw)
                if payload.get("success") is not True:
                    raise ValueError("Unsuccessful API response")
                messages = payload["data"]["emails"]
                if not isinstance(messages, list):
                    raise ValueError("Invalid inbox response")
                for message in messages:
                    if (parseaddr(str(message.get("from_address", "")))[1].lower() == sender.lower()
                            and subject_marker in str(message.get("subject", ""))
                            and float(message.get("timestamp", 0)) >= since
                            and message.get("id")):
                        return message
            except (ValueError, TypeError, KeyError, AttributeError) as error:
                raise RuntimeError("Invalid inbox response; stopped polling") from error
        elif status == 429 or status in (500, 502, 503, 504):
            if status == 429:
                try:
                    usage = json.loads(raw).get("usage") or {}
                except (ValueError, TypeError, AttributeError):
                    usage = {}
                if ((usage.get("daily_limit", 0) > 0 and usage.get("remaining_today") == 0)
                        or (usage.get("total_limit", 0) > 0 and usage.get("remaining_total") == 0)):
                    raise RuntimeError("API quota exhausted; stopped polling")
            delay = max(interval, retry_delay(headers.get("Retry-After")))
        else:
            raise RuntimeError(f"Inbox request returned HTTP {status}; stopped polling")
        if attempt + 1 == max_attempts or delay >= deadline - clock():
            break
        sleep(delay)
    raise TimeoutError("No matching message within the polling budget")


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--address", required=True)
    parser.add_argument("--sender", required=True)
    parser.add_argument("--marker", required=True)
    parser.add_argument("--since", type=float, required=True, help="Unix seconds recorded before triggering the email")
    args = parser.parse_args()
    try:
        wait_for_email(args.address, os.environ.get("CLEANTEMPMAIL_API_KEY", ""),
                       args.sender, args.marker, since=args.since)
        print("Matching test email received")
    except (ValueError, RuntimeError, TimeoutError) as error:
        parser.exit(1, str(error) + "\n")
