#!/usr/bin/env python3
"""Synthetic invoices API fixture 1.0.0 for an IDOR experiment. Binds IPv4 loopback only."""
import argparse
import json
import uuid
from http.server import BaseHTTPRequestHandler, HTTPServer

VERSION = "1.0.0"
IDENTITIES = {"demo-alice-token": "alice", "demo-bob-token": "bob"}
SEED = [
    ("1001", "alice", "Acme Synthetic Ltd", 12500, "open"),
    ("1002", "alice", "Acme Synthetic Ltd", 8400, "open"),
    ("1003", "bob", "Bobs Example Shop", 3100, "open"),
]


def build_invoices(id_style):
    invoices = {}
    for number, owner, customer, amount, status in SEED:
        key = number if id_style == "sequential" else str(uuid.uuid4())
        invoices[key] = {"id": key, "owner": owner, "customer": customer,
                         "amount_cents": amount, "status": status}
    return invoices


class Handler(BaseHTTPRequestHandler):
    server_version = "InvoiceFixture/" + VERSION
    sys_version = ""

    def identity(self):
        return IDENTITIES.get(self.headers.get("Authorization", "").removeprefix("Bearer "))

    def fetch(self, identity, invoice_id):
        """Return the invoice, or send the error response and return None.
        GET, PUT and DELETE all go through this one function."""
        invoice = self.server.invoices.get(invoice_id)
        if invoice is None:
            self.reply(404, {"error": "not_found"})
            return None
        if self.server.authz == "owner" and invoice["owner"] != identity:
            self.reply(403, {"error": "forbidden"})
            return None
        return invoice

    def route(self):
        identity = self.identity()
        if identity is None:
            self.reply(401, {"error": "authentication_required"})
            return None, None, False
        if self.path == "/invoices":
            return identity, None, True
        if self.path.startswith("/invoices/"):
            return identity, self.path[len("/invoices/"):], True
        self.reply(404, {"error": "not_found"})
        return None, None, False

    def do_GET(self):
        identity, invoice_id, ok = self.route()
        if not ok:
            return
        if invoice_id is None:
            rows = [i for i in self.server.invoices.values()
                    if self.server.authz != "owner" or i["owner"] == identity]
            return self.reply(200, {"invoices": rows})
        invoice = self.fetch(identity, invoice_id)
        if invoice is not None:
            self.reply(200, invoice)

    def do_PUT(self):
        identity, invoice_id, ok = self.route()
        if not ok:
            return
        if invoice_id is None:
            return self.reply(405, {"error": "method_not_allowed"})
        try:
            data = json.loads(self.rfile.read(int(self.headers.get("Content-Length", "0"))))
            amount, status = data["amount_cents"], data["status"]
            assert isinstance(amount, int) and status in ("open", "paid")
        except Exception:
            return self.reply(400, {"error": "bad_request"})
        invoice = self.fetch(identity, invoice_id)
        if invoice is not None:
            invoice["amount_cents"], invoice["status"] = amount, status
            self.reply(200, invoice)

    def do_DELETE(self):
        identity, invoice_id, ok = self.route()
        if not ok:
            return
        if invoice_id is None:
            return self.reply(405, {"error": "method_not_allowed"})
        invoice = self.fetch(identity, invoice_id)
        if invoice is not None:
            del self.server.invoices[invoice_id]
            self.reply(200, {"deleted": invoice_id})

    def reply(self, status, payload):
        body = (json.dumps(payload, sort_keys=True) + "\n").encode()
        self.send_response(status)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(body)))
        self.send_header("Cache-Control", "no-store")
        self.end_headers()
        self.wfile.write(body)

    def log_message(self, *_args):
        pass


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--ids", choices=("sequential", "uuid"), required=True)
    parser.add_argument("--authz", choices=("none", "owner"), required=True,
                        help="none: any authenticated user may use any invoice; owner: ownership check")
    parser.add_argument("--port", type=int, default=8765, help="0 chooses a free local port")
    args = parser.parse_args()
    server = HTTPServer(("127.0.0.1", args.port), Handler)
    server.authz = args.authz
    server.invoices = build_invoices(args.ids)
    print(json.dumps({"fixture_version": VERSION, "ids": args.ids, "authz": args.authz,
                      "base_url": "http://127.0.0.1:" + str(server.server_port)}), flush=True)
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        pass
    finally:
        server.server_close()


if __name__ == "__main__":
    main()
