#!/usr/bin/env python3
"""mock_olrapi_hifi.py — H4: higher-fidelity mock of the Obsidian Local REST API.

The shipped mock's PATCH is a "naive append": it appends the body to EOF instead of
inserting/replacing under the target heading, so the 0.9 surface (capture's
_append_to_existing, scope set's replace, the daily Agent-Log insert) is only ever
validated against behavior the real API does NOT exhibit. This subclass models the
real PATCH semantics so the eval can catch heading-insert / replace regressions.

Over the base mock (eval/mock_olrapi.py) this overrides do_PATCH only:
  * heading append   -> insert at the END of the target section's body (before the next
                        sibling/parent heading), NOT at EOF.
  * heading prepend  -> insert at the START of the section body (right after the heading).
  * heading replace  -> replace the section body, keep the heading line.
  * heading missing  -> 400 errorCode 40080 (the silent-write-loss trigger). [unchanged]
  * frontmatter replace -> parse the YAML block and set/replace the scalar; a replace on a
                        MISSING field returns 400 40080 (matches the real API, per SKILL.md).

Everything else (GET/PUT/POST/DELETE, directory listing, document-map, flaky/phantom
fault injection, debug reset) is inherited unchanged from the base handler.
"""
from __future__ import annotations

import argparse
import json
import re
from http.server import ThreadingHTTPServer

import mock_olrapi
from mock_olrapi import H as BaseH, STATE


def _heading(line: str):
    m = re.match(r"^(#{1,6})\s+(.*)$", line)
    return (len(m.group(1)), m.group(2).strip()) if m else (0, None)


def section_span(lines, target):
    """(start, end, level) for the section under `target` (an "A::B" path; matched on its
    leaf). Body lines are lines[start+1:end]. None if the heading is absent."""
    leaf = target.split("::")[-1].strip()
    start = level = None
    for i, line in enumerate(lines):
        lv, txt = _heading(line)
        if lv and txt == leaf:
            start, level = i, lv
            break
    if start is None:
        return None
    end = len(lines)
    for j in range(start + 1, len(lines)):
        lv, _ = _heading(lines[j])
        if lv and lv <= level:
            end = j
            break
    return start, end, level


def set_frontmatter(text: str, field: str, raw_value: str):
    """Return (new_text, existed). raw_value is the JSON the client PATCHed."""
    try:
        value = json.loads(raw_value)
    except Exception:
        value = raw_value.strip().strip('"')
    rendered = value if isinstance(value, str) else json.dumps(value)
    new_line = f"{field}: {rendered}"
    if not text.startswith("---"):
        return f"---\n{new_line}\n---\n\n{text}", False
    end = text.find("\n---", 3)
    head = text[4:end] if end != -1 else text
    rest = text[end:] if end != -1 else "\n---\n"
    out, existed = [], False
    for line in head.splitlines():
        if re.match(rf"^{re.escape(field)}\s*:", line):
            out.append(new_line)
            existed = True
        else:
            out.append(line)
    if not existed:
        out.append(new_line)
    return "---\n" + "\n".join(out) + rest, existed


class H(BaseH):
    def do_PATCH(self):
        vp = self._vpath()
        body = self._read_body()
        if vp is None:
            return self._json(400, {"message": "bad path"})
        ttype = self.headers.get("Target-Type", "")
        op = self.headers.get("Operation", "append")
        target = self.headers.get("Target", "")
        cur = STATE.get(vp, "")

        if ttype == "heading":
            lines = cur.split("\n")
            span = section_span(lines, target)
            if span is None:
                return self._json(400, {"errorCode": 40080, "message": "invalid-target"})
            start, end, _ = span
            payload = body.strip("\n").split("\n") if body.strip("\n") else [""]
            if op == "replace":
                lines[start + 1:end] = payload
            elif op == "prepend":
                lines[start + 1:start + 1] = payload
            else:  # append at the tail of the section body (before the next heading)
                ins = end
                while ins > start + 1 and lines[ins - 1].strip() == "":
                    ins -= 1
                lines[ins:ins] = payload
            STATE[vp] = "\n".join(lines)
            return self._send(200, "")

        if ttype == "frontmatter":
            new, existed = set_frontmatter(cur, target, body)
            if op == "replace" and not existed:
                return self._json(400, {"errorCode": 40080, "message": "invalid-target"})
            STATE[vp] = new
            return self._send(200, "")

        STATE[vp] = cur + "\n" + body
        return self._send(200, "")


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--port", type=int, default=8800)
    a = ap.parse_args()
    srv = ThreadingHTTPServer(("127.0.0.1", a.port), H)
    print(f"mock-olrapi-hifi listening on http://127.0.0.1:{a.port}", flush=True)
    srv.serve_forever()


if __name__ == "__main__":
    main()
