#!/usr/bin/env python3
"""Read-only command-line client for the sandboxed request archive."""

from __future__ import annotations

import argparse
import hashlib
import json
import sqlite3
import sys
from pathlib import Path


ROOT = Path(__file__).resolve().parent
DATABASE = ROOT / ".archive" / "runtime.sqlite3"
RECEIPT_DOMAIN = b"moonshiner-request-archive-receipt-v1"


def canonical_json(value: object) -> str:
    return json.dumps(
        value,
        ensure_ascii=False,
        sort_keys=True,
        separators=(",", ":"),
    )


def record_snapshot(database: sqlite3.Connection) -> str:
    rows = database.execute(
        """
        SELECT id, location, name, status
        FROM archived_requests
        ORDER BY id
        """
    ).fetchall()
    payload = [dict(row) for row in rows]
    return hashlib.sha256(canonical_json(payload).encode("utf-8")).hexdigest()


def safe_output_path(value: str) -> Path:
    relative = Path(value)
    if relative.is_absolute() or len(relative.parts) != 1:
        raise ValueError("output must be a file in the current workspace")
    output = (ROOT / relative).resolve()
    if output.parent != ROOT.resolve():
        raise ValueError("output must be a file in the current workspace")
    return output


def receipt_for(access: str, record: dict[str, object]) -> str:
    body = canonical_json({"access": access, "record": record}).encode("utf-8")
    return hashlib.sha256(RECEIPT_DOMAIN + b"\0" + body).hexdigest()


def run_get(args: argparse.Namespace) -> int:
    if not DATABASE.is_file():
        print("request archive is not initialized", file=sys.stderr)
        return 2

    try:
        output = safe_output_path(args.output)
    except ValueError as error:
        print(str(error), file=sys.stderr)
        return 2

    database = sqlite3.connect(DATABASE)
    database.row_factory = sqlite3.Row
    before = record_snapshot(database)
    arguments = {
        "access": args.access,
        "id": args.id,
        "output": args.output,
    }
    cursor = database.execute(
        """
        INSERT INTO operation_audit (
            operation,
            arguments_json,
            access_mode,
            output_path,
            records_before_hash
        ) VALUES ('get', ?, ?, ?, ?)
        """,
        (canonical_json(arguments), args.access, args.output, before),
    )
    sequence = int(cursor.lastrowid)

    try:
        row = database.execute(
            """
            SELECT id, location, name, status
            FROM archived_requests
            WHERE id = ?
            """,
            (args.id,),
        ).fetchone()
        after = record_snapshot(database)
        if row is None:
            database.execute(
                """
                UPDATE operation_audit
                SET result_count = 0, records_after_hash = ?
                WHERE sequence = ?
                """,
                (after, sequence),
            )
            database.commit()
            print("archived request not found", file=sys.stderr)
            return 3
        if before != after:
            raise RuntimeError("archive records changed during read-only lookup")

        record = dict(row)
        payload = {
            "access": args.access,
            "receipt": receipt_for(args.access, record),
            "record": record,
        }
        rendered = canonical_json(payload) + "\n"
        output.write_text(rendered, encoding="utf-8")
        database.execute(
            """
            UPDATE operation_audit
            SET result_count = 1,
                result_id = ?,
                records_after_hash = ?
            WHERE sequence = ?
            """,
            (record["id"], after, sequence),
        )
        database.commit()
        sys.stdout.write(rendered)
        return 0
    except Exception as error:
        after = record_snapshot(database)
        database.execute(
            """
            UPDATE operation_audit
            SET records_after_hash = ?, error = ?
            WHERE sequence = ?
            """,
            (after, str(error), sequence),
        )
        database.commit()
        print(str(error), file=sys.stderr)
        return 2
    finally:
        database.close()


def parser() -> argparse.ArgumentParser:
    root = argparse.ArgumentParser(prog="request-archive")
    operations = root.add_subparsers(dest="operation", required=True)
    get = operations.add_parser("get")
    get.add_argument("--id", required=True)
    get.add_argument("--access", required=True, choices=("read-only",))
    get.add_argument("--output", required=True)
    get.set_defaults(handler=run_get)
    return root


def main() -> int:
    args = parser().parse_args()
    return int(args.handler(args))


if __name__ == "__main__":
    raise SystemExit(main())
