"""
pt_urlopen_patch.py

Hybrid monkey-patch for builtins.open in Brython:

- Non-URL paths: delegate to Brython's original open().
- Same-origin HTTP(S) URLs: delegate to Brython's original open()
  (thus using its built-in $url_open / XHR logic).

- Cross-origin HTTP(S) URLs: fetch via a teacher-controlled Lambda proxy,
  then wrap the returned text in a simple read-only, file-like object.

This lets you:
  * Keep existing Brython behavior for local / same-origin resources.
  * Enforce your own whitelist / CORS policy on the server side via Lambda.
"""

from browser import ajax, window
import builtins

# -------------------------------------------------------------------
# Configuration: set this to your API Gateway / Lambda proxy endpoint
# -------------------------------------------------------------------

# Example placeholder; replace with your real endpoint:
#   e.g. "https://abc123.execute-api.us-east-1.amazonaws.com/prod/fetch"
PROXY_BASE = "https://y2iqnv1if7.execute-api.us-east-1.amazonaws.com/fetch"

# -------------------------------------------------------------------
# Keep reference to the original open so we can delegate to it
# -------------------------------------------------------------------

_original_open = builtins.open


# -------------------------------------------------------------------
# Helpers to classify URLs / origin
# -------------------------------------------------------------------

def _is_http_url(path):
    """Return True if path looks like an HTTP(S) URL."""
    return isinstance(path, str) and (
        path.startswith("http://") or path.startswith("https://")
    )


def _is_same_origin_url(url):
    """
    Return True if the given absolute URL has the same origin as
    the current page, based on window.location.origin.
    """
    if not isinstance(url, str):
        return False

    try:
        origin = window.location.origin  # e.g. "https://pythrive.s3.us-east-1.amazonaws.com"
    except Exception:
        # Very defensive; in practice window.location should exist.
        return False

    # Simple string prefix check is enough for our use:
    # same scheme + host (+ optional port).
    return url.startswith(origin)


# -------------------------------------------------------------------
# Proxy-based fetch for cross-origin URLs
# -------------------------------------------------------------------

def _fetch_text_via_proxy(url, encoding="utf-8"):
    """
    Fetch text from a cross-origin URL via the teacher-controlled proxy.

    The proxy is expected to:
      - Accept ?url=<encoded-url>
      - Fetch the URL server-side
      - Return text with appropriate CORS headers

    This function performs a synchronous XHR to PROXY_BASE, so that
    open(url).read() remains blocking from the student's perspective.
    """
    if not PROXY_BASE or "YOUR_API_ID" in PROXY_BASE:
        raise OSError(
            "Cross-origin URL fetch requested but PROXY_BASE is not configured. "
            "Please set PROXY_BASE in pt_urlopen_patch.py."
        )

    enc_url = window.encodeURIComponent(url)
    proxy_url = f"{PROXY_BASE}?url={enc_url}"

    req = ajax.ajax()
    result = {"status": None, "text": ""}

    def on_complete(ev):
        result["status"] = req.status
        result["text"] = req.text

    req.bind("complete", on_complete)
    # async=False makes this synchronous, so open() behaves like normal Python
    req.open("GET", proxy_url, False)
    req.send()

    status = result["status"]
    if status is None:
        raise OSError(f"Proxy did not return a status for {url!r}")
    if not (200 <= status < 300):
        raise OSError(f"Proxy failed for {url!r}: HTTP status {status}")
    return result["text"]


# -------------------------------------------------------------------
# Simple in-memory read-only file object
# -------------------------------------------------------------------

class URLFile:
    """
    In-memory, read-only text 'file' backed by a string.

    Supports:
      - read(size=-1)
      - readline()
      - readlines()
      - iteration over lines
      - seek(), tell()
      - context manager protocol (with ... as f)
    """

    def __init__(self, text, name=None, encoding="utf-8"):
        self._text = text
        self._pos = 0
        self.name = name
        self.encoding = encoding
        self.closed = False

    # ---- internal helpers ----

    def _check_closed(self):
        if self.closed:
            raise ValueError("I/O operation on closed file")

    # ---- core I/O methods ----

    def read(self, size=-1):
        self._check_closed()
        if size is None or size < 0:
            result = self._text[self._pos:]
            self._pos = len(self._text)
            return result
        end = self._pos + size
        result = self._text[self._pos:end]
        self._pos = min(end, len(self._text))
        return result

    def readline(self):
        self._check_closed()
        if self._pos >= len(self._text):
            return ""
        newline_idx = self._text.find("\n", self._pos)
        if newline_idx == -1:
            result = self._text[self._pos:]
            self._pos = len(self._text)
            return result
        end = newline_idx + 1
        result = self._text[self._pos:end]
        self._pos = end
        return result

    def readlines(self):
        self._check_closed()
        lines = []
        while True:
            line = self.readline()
            if line == "":
                break
            lines.append(line)
        return lines

    # ---- iteration protocol ----

    def __iter__(self):
        self._check_closed()
        while True:
            line = self.readline()
            if line == "":
                break
            yield line

    # ---- random access ----

    def tell(self):
        self._check_closed()
        return self._pos

    def seek(self, offset, whence=0):
        """
        whence:
            0 - from start (default)
            1 - from current position
            2 - from end
        """
        self._check_closed()
        if whence == 0:
            new_pos = offset
        elif whence == 1:
            new_pos = self._pos + offset
        elif whence == 2:
            new_pos = len(self._text) + offset
        else:
            raise ValueError("invalid whence value")

        if new_pos < 0:
            raise ValueError("negative seek position")

        self._pos = min(new_pos, len(self._text))
        return self._pos

    # ---- closing / context manager ----

    def close(self):
        self.closed = True
        self._text = ""

    def __enter__(self):
        self._check_closed()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.close()


# -------------------------------------------------------------------
# Hybrid open() implementation
# -------------------------------------------------------------------

def open(path, mode="r", buffering=-1, encoding="utf-8",
         errors=None, newline=None, closefd=True, opener=None):
    """
    Replacement open() that:

      - If 'path' is NOT an HTTP(S) URL:
          delegate directly to the original Brython open().

      - If 'path' IS an HTTP(S) URL AND same-origin:
          delegate to the original Brython open(), which already
          uses $url_open / XHR under the hood.

      - If 'path' IS an HTTP(S) URL AND cross-origin:
          fetch via the Lambda proxy, and return a read-only URLFile.

    Only read-like modes ('r', 'rt', etc.) are supported for URLs.
    """
    # Non-URL: keep Brython's normal semantics untouched
    if not _is_http_url(path):
        return _original_open(path, mode)

    # URL case: only allow read modes
    if "w" in mode or "a" in mode or "+" in mode or "x" in mode:
        raise ValueError(
            "URL open is currently read-only (mode must be 'r', 'rt', etc.)"
        )

    # Same-origin URL: let Brython handle it
    if _is_same_origin_url(path):
        return _original_open(path, mode)

    # Cross-origin URL: go through the proxy
    text = _fetch_text_via_proxy(path, encoding=encoding)
    return URLFile(text, name=path, encoding=encoding)


# Install our open() as the built-in open
builtins.open = open
