import base64
import hashlib
import hmac
import html
import json
import os
import time
from http import cookies
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from typing import Optional
from urllib.parse import urlencode, urlparse
from urllib.request import urlopen


PORT = int(os.environ.get("PORT", "8310"))
APP_ID = "python-app"
AUTH_LOGIN = "http://127.0.0.1:8300/login"
AUTH_VALIDATE = "http://127.0.0.1:8300/validate"
CALLBACK = "http://127.0.0.1:8310/callback"
SECRET = b"python-app-local-session-secret"


def sign(value: str) -> str:
    return hmac.new(SECRET, value.encode("utf-8"), hashlib.sha256).hexdigest()


def encode_session(payload: dict) -> str:
    raw = base64.urlsafe_b64encode(json.dumps(payload).encode("utf-8")).decode("utf-8")
    return f"{raw}.{sign(raw)}"


def decode_session(token: str) -> Optional[dict]:
    if "." not in token:
        return None
    raw, signature = token.rsplit(".", 1)
    if not hmac.compare_digest(signature, sign(raw)):
        return None
    return json.loads(base64.urlsafe_b64decode(raw.encode("utf-8")))


class PythonAppHandler(BaseHTTPRequestHandler):
    def send_html(self, body: str, status: int = 200, headers: Optional[dict] = None) -> None:
        content = body.encode("utf-8")
        self.send_response(status)
        self.send_header("Content-Type", "text/html; charset=utf-8")
        self.send_header("Content-Length", str(len(content)))
        if headers:
            for key, value in headers.items():
                self.send_header(key, value)
        self.end_headers()
        self.wfile.write(content)

    def redirect(self, target: str, headers: Optional[dict] = None) -> None:
        self.send_response(302)
        self.send_header("Location", target)
        if headers:
            for key, value in headers.items():
                self.send_header(key, value)
        self.end_headers()

    def current_user(self) -> Optional[str]:
        jar = cookies.SimpleCookie(self.headers.get("Cookie", ""))
        token = jar.get("python_app_session")
        payload = decode_session(token.value) if token else None
        return payload.get("username") if payload else None

    def do_GET(self) -> None:
        parsed = urlparse(self.path)
        if parsed.path == "/callback":
            self.handle_callback(parsed.query)
        elif parsed.path == "/logout":
            self.redirect("/", {"Set-Cookie": "python_app_session=; Max-Age=0; Path=/; HttpOnly; SameSite=Lax"})
        else:
            self.render_app()

    def render_app(self) -> None:
        user = self.current_user()
        if not user:
            login_url = AUTH_LOGIN + "?" + urlencode({"app_id": APP_ID, "redirect_uri": CALLBACK})
            self.redirect(login_url)
            return
        self.send_html(layout("Python 业务应用", f"""
          <section class="hero"><h1>Python 业务应用</h1><p>当前用户：<strong>{html.escape(user)}</strong>。这个登录态是 Python 应用自己的本地 session，由统一 SSO 的 ticket 换来。</p><div class="actions"><a href="http://127.0.0.1:8320/">访问 PHP 应用</a><a href="http://127.0.0.1:8300/">查看认证中心</a><a href="/logout">退出 Python 应用</a></div></section>
          {docs()}
        """))

    def handle_callback(self, query: str) -> None:
        params = dict(item.split("=", 1) for item in query.split("&") if "=" in item)
        ticket = params.get("ticket", "")
        validate_url = AUTH_VALIDATE + "?" + urlencode({"app_id": APP_ID, "ticket": ticket})
        try:
            with urlopen(validate_url, timeout=5) as response:
                data = json.loads(response.read().decode("utf-8"))
        except Exception:
            self.send_html(layout("校验失败", "<section class='panel'><h1>ticket 校验失败</h1></section>"), 401)
            return
        session = encode_session({"username": data["username"], "iat": int(time.time())})
        self.redirect("/", {"Set-Cookie": f"python_app_session={session}; Path=/; HttpOnly; SameSite=Lax"})


def docs() -> str:
    return """
      <section class="grid"><article class="panel"><h2>这个应用做什么</h2><p>它只检查自己的 python_app_session。没有 session 就跳转统一认证中心，不保存用户密码。</p></article><article class="panel"><h2>主要代码段</h2><pre><code>login_url = AUTH_LOGIN + '?app_id=python-app'
validate_url = AUTH_VALIDATE + '?ticket=' + ticket
set_cookie('python_app_session')</code></pre></article><article class="panel"><h2>逻辑</h2><ol><li>访问 Python 应用</li><li>跳转 SSO</li><li>SSO 回跳 callback</li><li>Python 调用 /validate</li><li>写入本地 session</li></ol></article><article class="panel"><h2>注意事项</h2><p>业务应用不要自己信任 ticket 内容，必须调用认证中心校验，且 ticket 校验后立即失效。</p></article></section>
    """


def layout(title: str, content: str) -> str:
    return f"<!DOCTYPE html><html lang='zh-CN'><head><meta charset='UTF-8'><meta name='viewport' content='width=device-width, initial-scale=1.0'><title>{html.escape(title)}</title><style>*{{box-sizing:border-box;margin:0;padding:0}}body{{min-height:100vh;background:#f5f2ea;color:#1f2a24;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','Microsoft YaHei',sans-serif;line-height:1.7}}.page{{width:min(1120px,calc(100% - 32px));margin:0 auto;padding:34px 0 48px}}.hero,.panel{{border:1px solid #d7cdbc;border-radius:8px;background:#fffdf8;padding:24px}}.hero{{margin-bottom:18px}}h1{{font-size:clamp(30px,5vw,52px);line-height:1.1;margin-bottom:12px}}h2{{font-size:20px;margin-bottom:8px}}p,li{{color:#53645b}}.actions{{display:flex;flex-wrap:wrap;gap:10px;margin-top:16px}}a{{display:inline-block;border-radius:6px;background:#8a5a1f;color:#fff;padding:10px 16px;font-weight:800;text-decoration:none}}.grid{{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px}}ol{{padding-left:20px}}pre{{overflow-x:auto;border-radius:6px;background:#111827;color:#d7e7ff;padding:14px;font:13px/1.55 Consolas,Monaco,monospace}}@media(max-width:760px){{.grid{{grid-template-columns:1fr}}}}</style></head><body><main class='page'>{content}</main></body></html>"


if __name__ == "__main__":
    print(f"Python app running at http://127.0.0.1:{PORT}")
    ThreadingHTTPServer(("127.0.0.1", PORT), PythonAppHandler).serve_forever()
