"""정렬 감식 과제 클라이언트 — 표준 라이브러리만 쓴다(설치할 것 없음).

    import sortlab
    lab = sortlab.Lab("slk_...")                      # Algo Tutor 시험 탭 「파이썬 접속 키」
    r = lab.query("P", [3, 1, 2])                   # 연습 함수(선택 정렬) — 예산 없음
    r = lab.query("A", list(range(1000)))             # 함수 A — 예산 30번
    print(r)   # {'query_id': 'A001', 'n': 1000, 'comparisons': ..., 'writes': ..., 'remaining': 29}
    lab.status()                                      # 남은 질의 수·제출 상태
    rows = lab.history("A", with_data=True)           # 서버에 남은 내 질의(입력 포함)
    lab.template("answers.json")                      # 답안 틀 내려받기
    lab.check("answers.json")                         # 형식 검사만(제출 안 함)
    lab.submit("answers.json")                        # 제출(마감 전까지 다시 내면 마지막 것이 채점)

접속 키는 비밀번호와 같다. 코드를 공유할 때 키가 들어가지 않게 조심할 것.
질의 결과는 이 파일과 같은 폴더의 sortlab_log.csv에도 한 줄씩 자동으로 남는다.
서버 주소를 바꾸려면 환경변수 SORTLAB_SERVER.
"""
from __future__ import annotations

import csv
import json
import os
import time
import urllib.error
import urllib.request

SERVER = os.environ.get("SORTLAB_SERVER", "https://sortlab.chansu.app").rstrip("/")
LOG = os.path.join(os.getcwd(), "sortlab_log.csv")


class SortLabError(Exception):
    pass


def _call(method, path, body=None, token=None, raw=False):
    data = None
    headers = {"Accept": "application/json"}
    if body is not None:
        data = (body if isinstance(body, bytes) else json.dumps(body).encode())
        headers["Content-Type"] = "application/json"
    if token:
        headers["Authorization"] = "Bearer " + token
    req = urllib.request.Request(SERVER + path, data=data, headers=headers, method=method)
    for attempt in range(3):
        try:
            with urllib.request.urlopen(req, timeout=60) as r:
                return json.loads(r.read().decode())
        except urllib.error.HTTPError as e:
            text = e.read().decode(errors="replace")
            try:
                j = json.loads(text)
            except ValueError:
                j = {"detail": text[:300]}
            if e.code == 422 and raw:
                return j
            if e.code == 429 and attempt < 2:
                time.sleep(3)
                continue
            msg = j.get("detail") if isinstance(j.get("detail"), str) else json.dumps(j, ensure_ascii=False)
            raise SortLabError(f"[{e.code}] {msg}") from None
        except urllib.error.URLError as e:
            if attempt < 2:
                time.sleep(2)
                continue
            raise SortLabError(f"서버에 연결할 수 없습니다({SERVER}): {e.reason}") from None


def info():
    """과제 정보: 후보 목록, 예산, n 상한, 연산 규칙, 마감."""
    return _call("GET", "/api/info")


class Lab:
    def __init__(self, key_or_student_no: str, password: str | None = None):
        """Lab(접속 키). 예전에 비밀번호를 만든 학생은 Lab(학번, 비밀번호)도 된다."""
        first = str(key_or_student_no).strip()
        if password is not None:
            self.token = _call("POST", "/api/login", {"student_no": first, "password": password})["token"]
            self.student_no = first
        elif first.startswith("slk_"):
            self.token = first
            self.student_no = _call("GET", "/api/me", token=first)["student_no"]
        else:
            raise SortLabError("접속 키가 아닙니다. Algo Tutor 시험 탭 「파이썬 접속 키」에서 복사하세요(slk_로 시작).")

    def query(self, label: str, data) -> dict:
        data = [int(x) for x in data]
        r = _call("POST", "/api/query", {"label": label, "data": data}, self.token)
        new = not os.path.exists(LOG)
        with open(LOG, "a", newline="", encoding="utf-8") as fp:
            w = csv.writer(fp)
            if new:
                w.writerow(["time", "query_id", "label", "n", "comparisons", "writes", "head"])
            w.writerow([time.strftime("%Y-%m-%d %H:%M:%S"), r["query_id"], r["label"], r["n"],
                        r["comparisons"], r["writes"], " ".join(map(str, data[:8])) + (" …" if len(data) > 8 else "")])
        return r

    def status(self) -> dict:
        r = _call("GET", "/api/me", token=self.token)
        print(f"{r['student_no']} {r['name']} · 마감 {r['deadline'] or '미정'}")
        for L, f in r["functions"].items():
            print(f"  함수 {L}: {f['used']}/{f['budget']} 사용")
        print(f"  연습 P: 오늘 {r['practice_today']['used']}/{r['practice_today']['limit']}")
        s = r["submission"]
        if s:
            print(f"  제출: {s['submitted_at']} (경고 {len(s['warnings'])}개)")
        else:
            print("  제출: 아직 안 함")
        return r

    def history(self, label: str | None = None, with_data: bool = False) -> list[dict]:
        q = []
        if label:
            q.append("label=" + label)
        if with_data:
            q.append("with_data=true")
        return _call("GET", "/api/queries" + ("?" + "&".join(q) if q else ""), token=self.token)

    def template(self, path: str = "answers.json"):
        t = _call("GET", "/api/answers/template", token=self.token)
        if os.path.exists(path):
            raise SortLabError(f"{path}가 이미 있습니다 — 덮어쓰지 않습니다")
        with open(path, "w", encoding="utf-8") as fp:
            json.dump(t, fp, ensure_ascii=False, indent=2)
        print(f"→ {path} (algorithm·k·confidence·reasoning을 채우고, 기각한 후보의 queries에 근거 질의 id를 넣으세요)")

    def _load(self, path):
        with open(path, "rb") as fp:
            return fp.read()

    def check(self, path: str = "answers.json") -> dict:
        r = _call("POST", "/api/answers/check", self._load(path), self.token)
        _print_result(r)
        return r

    def submit(self, path: str = "answers.json") -> dict:
        r = _call("POST", "/api/answers", self._load(path), self.token, raw=True)
        _print_result(r)
        if r.get("ok"):
            print(r.get("message"))
        return r


def _print_result(r):
    if r.get("errors"):
        print(f"오류 {len(r['errors'])}개 — 고쳐야 제출됩니다:")
        for e in r["errors"]:
            print("  ✗", e)
    if r.get("warnings"):
        print(f"경고 {len(r['warnings'])}개 — 제출은 되지만 점수를 잃을 수 있습니다:")
        for w in r["warnings"]:
            print("  !", w)
    if r.get("ok") and not r.get("errors"):
        print("형식 이상 없음" if not r.get("submitted_at") else f"제출됨 {r['submitted_at']}")
