import collections
from http import cookiejar
from typing import Optional

from mitmproxy import http, flowfilter, ctx, exceptions
from mitmproxy.net.http import cookies

TOrigin = tuple[str, int, str]


def ckey(attrs: dict[str, str], f: http.HTTPFlow) -> TOrigin:
    """
    Returns a (domain, port, path) tuple.
    """
    domain = f.request.host
    path = "/"
    if "domain" in attrs:
        domain = attrs["domain"]
    if "path" in attrs:
        path = attrs["path"]
    return (domain, f.request.port, path)


def domain_match(a: str, b: str) -> bool:
    if cookiejar.domain_match(a, b):  # type: ignore
        return True
    elif cookiejar.domain_match(a, b.strip(".")):  # type: ignore
        return True
    return False


class StickyCookie:
    def __init__(self):
        self.jar: dict[TOrigin, dict[str, str]] = collections.defaultdict(dict)
        self.flt: Optional[flowfilter.TFilter] = None

    def load(self, loader):
        loader.add_option(
            "stickycookie",
            Optional[str],
            None,
            "Set sticky cookie filter. Matched against requests.",
        )

    def configure(self, updated):
        if "stickycookie" in updated:
            if ctx.options.stickycookie:
                try:
                    self.flt = flowfilter.parse(ctx.options.stickycookie)
                except ValueError as e:
                    raise exceptions.OptionsError(str(e)) from e
            else:
                self.flt = None

    def response(self, flow: http.HTTPFlow):
        assert flow.response
        if self.flt:
            for name, (value, attrs) in flow.response.cookies.items(multi=True):
                # FIXME: We now know that Cookie.py screws up some cookies with
                # valid RFC 822/1123 datetime specifications for expiry. Sigh.
                dom_port_path = ckey(attrs, flow)

                if domain_match(flow.request.host, dom_port_path[0]):
                    if cookies.is_expired(attrs):
                        # Remove the cookie from jar
                        self.jar[dom_port_path].pop(name, None)

                        # If all cookies of a dom_port_path have been removed
                        # then remove it from the jar itself
                        if not self.jar[dom_port_path]:
                            self.jar.pop(dom_port_path, None)
                    else:
                        self.jar[dom_port_path][name] = value

    def request(self, flow: http.HTTPFlow):
        if self.flt:
            cookie_list: list[tuple[str, str]] = []
            if flowfilter.match(self.flt, flow):
                for (domain, port, path), c in self.jar.items():
                    match = [
                        domain_match(flow.request.host, domain),
                        flow.request.port == port,
                        flow.request.path.startswith(path),
                    ]
                    if all(match):
                        cookie_list.extend(c.items())
            if cookie_list:
                # FIXME: we need to formalise this...
                flow.metadata["stickycookie"] = True
                flow.request.headers["cookie"] = cookies.format_cookie_header(
                    cookie_list
                )
