#!/usr/bin/env python3
"""Executable client for the sandboxed item register."""

from __future__ import annotations

import argparse
import fcntl
import hashlib
import hmac
import json
from pathlib import Path
import sqlite3
import sys
from typing import Any


ROOT = Path(__file__).resolve().parent
PROTECTED = ROOT / ".protected"
SEED_PATH = PROTECTED / "items.json"
KEY_PATH = PROTECTED / "audit.key"
RUNTIME = ROOT / ".inventory-runtime"
DATABASE = RUNTIME / "register.sqlite3"
AUDIT = RUNTIME / "audit.jsonl"
LOCK = RUNTIME / ".lock"

RECORD_FIELDS = (
    "id",
    "name",
    "location",
    "status",
    "date",
    "category",
    "quantity",
    "unit",
    "bin",
    "notes",
)


class OperationError(RuntimeError):
    """A user-facing item-register error."""


def canonical(value: Any) -> bytes:
    return json.dumps(
        value,
        ensure_ascii=False,
        sort_keys=True,
        separators=(",", ":"),
    ).encode("utf-8")


def digest(value: Any) -> str:
    return hashlib.sha256(canonical(value)).hexdigest()


def load_seed() -> list[dict[str, str]]:
    try:
        payload = json.loads(SEED_PATH.read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError) as error:
        raise OperationError(f"protected item seed is unavailable: {error}") from error
    if not isinstance(payload, dict) or payload.get("schema_version") != 1:
        raise OperationError("protected item seed has an unsupported schema")
    records = payload.get("records")
    if not isinstance(records, list) or not records:
        raise OperationError("protected item seed has no records")

    normalized: list[dict[str, str]] = []
    seen: set[str] = set()
    for item in records:
        if not isinstance(item, dict) or set(item) != set(RECORD_FIELDS):
            raise OperationError("protected item seed contains an invalid record")
        record = {field: item[field] for field in RECORD_FIELDS}
        if not all(isinstance(value, str) for value in record.values()):
            raise OperationError("protected item record values must be strings")
        if record["id"] in seen:
            raise OperationError("protected item seed contains duplicate stable IDs")
        seen.add(record["id"])
        normalized.append(record)
    return normalized


def initialize_database(
    connection: sqlite3.Connection, records: list[dict[str, str]]
) -> None:
    connection.executescript(
        """
        PRAGMA foreign_keys = ON;
        CREATE TABLE IF NOT EXISTS items (
            id TEXT PRIMARY KEY,
            name TEXT NOT NULL,
            location TEXT NOT NULL,
            status TEXT NOT NULL,
            date TEXT NOT NULL,
            category TEXT NOT NULL,
            quantity TEXT NOT NULL,
            unit TEXT NOT NULL,
            bin TEXT NOT NULL,
            notes TEXT NOT NULL
        );
        CREATE TABLE IF NOT EXISTS notifications (
            sequence INTEGER PRIMARY KEY AUTOINCREMENT,
            item_id TEXT NOT NULL REFERENCES items(id),
            message TEXT NOT NULL
        );
        CREATE TABLE IF NOT EXISTS register_meta (
            key TEXT PRIMARY KEY,
            value TEXT NOT NULL
        );
        """
    )
    count = connection.execute("SELECT COUNT(*) FROM items").fetchone()[0]
    seed_sha256 = hashlib.sha256(SEED_PATH.read_bytes()).hexdigest()
    if count == 0:
        connection.executemany(
            """
            INSERT INTO items (
                id, name, location, status, date, category, quantity, unit,
                bin, notes
            ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
            """,
            [
                tuple(record[field] for field in RECORD_FIELDS)
                for record in records
            ],
        )
        connection.execute(
            "INSERT OR REPLACE INTO register_meta(key, value) VALUES (?, ?)",
            ("seed_sha256", seed_sha256),
        )
        connection.commit()
        return

    marker = connection.execute(
        "SELECT value FROM register_meta WHERE key = 'seed_sha256'"
    ).fetchone()
    if marker is None or marker[0] != seed_sha256:
        raise OperationError("runtime item register does not match the protected seed")


def connect(records: list[dict[str, str]]) -> sqlite3.Connection:
    connection = sqlite3.connect(DATABASE)
    connection.row_factory = sqlite3.Row
    initialize_database(connection, records)
    return connection


def row_to_record(row: sqlite3.Row) -> dict[str, str]:
    return {field: row[field] for field in RECORD_FIELDS}


def logical_state(connection: sqlite3.Connection) -> dict[str, Any]:
    rows = connection.execute(
        """
        SELECT id, name, location, status, date, category, quantity, unit,
               bin, notes
        FROM items
        ORDER BY id
        """
    ).fetchall()
    notices = connection.execute(
        """
        SELECT sequence, item_id, message
        FROM notifications
        ORDER BY sequence
        """
    ).fetchall()
    return {
        "records": [row_to_record(row) for row in rows],
        "notifications": [
            {
                "sequence": row["sequence"],
                "item_id": row["item_id"],
                "message": row["message"],
            }
            for row in notices
        ],
    }


def perform(
    connection: sqlite3.Connection,
    action: str,
    arguments: argparse.Namespace,
) -> tuple[dict[str, Any], dict[str, Any]]:
    if action == "search":
        request = {"name": arguments.name, "location": arguments.location}
        rows = connection.execute(
            """
            SELECT id, name, location, status
            FROM items
            WHERE name = ? AND location = ?
            ORDER BY id
            """,
            (arguments.name, arguments.location),
        ).fetchall()
        matches = [
            {
                "id": row["id"],
                "name": row["name"],
                "location": row["location"],
                "status": row["status"],
            }
            for row in rows
        ]
        return request, {"match_count": len(matches), "matches": matches}

    if action == "get":
        request = {"id": arguments.id}
        row = connection.execute(
            """
            SELECT id, name, location, status, date, category, quantity, unit,
                   bin, notes
            FROM items
            WHERE id = ?
            """,
            (arguments.id,),
        ).fetchone()
        return request, {"record": row_to_record(row) if row else None}

    if action == "list":
        request = {}
        rows = connection.execute(
            """
            SELECT id, name, location, status
            FROM items
            ORDER BY id
            """
        ).fetchall()
        return request, {
            "records": [
                {
                    "id": row["id"],
                    "name": row["name"],
                    "location": row["location"],
                    "status": row["status"],
                }
                for row in rows
            ]
        }

    if action == "availability":
        request = {"id": arguments.id}
        row = connection.execute(
            """
            SELECT id, status, quantity, unit
            FROM items
            WHERE id = ?
            """,
            (arguments.id,),
        ).fetchone()
        return request, {
            "availability": (
                {
                    "id": row["id"],
                    "status": row["status"],
                    "quantity": row["quantity"],
                    "unit": row["unit"],
                }
                if row
                else None
            )
        }

    if action == "update":
        request = {"id": arguments.id, "status": arguments.status}
        cursor = connection.execute(
            "UPDATE items SET status = ? WHERE id = ?",
            (arguments.status, arguments.id),
        )
        return request, {
            "updated": cursor.rowcount == 1,
            "id": arguments.id if cursor.rowcount == 1 else None,
            "status": arguments.status if cursor.rowcount == 1 else None,
        }

    if action == "notify":
        request = {"id": arguments.id, "message": arguments.message}
        exists = connection.execute(
            "SELECT 1 FROM items WHERE id = ?",
            (arguments.id,),
        ).fetchone()
        if exists is None:
            return request, {"sent": False, "id": None}
        cursor = connection.execute(
            "INSERT INTO notifications(item_id, message) VALUES (?, ?)",
            (arguments.id, arguments.message),
        )
        return request, {
            "sent": True,
            "id": arguments.id,
            "notification_sequence": cursor.lastrowid,
        }

    raise OperationError(f"unsupported item-register operation: {action}")


def next_sequence() -> int:
    if not AUDIT.exists():
        return 1
    try:
        lines = AUDIT.read_text(encoding="utf-8").splitlines()
    except OSError as error:
        raise OperationError(f"cannot read execution evidence: {error}") from error
    if any(not line for line in lines):
        raise OperationError("execution evidence contains an empty entry")
    return len(lines) + 1


def append_evidence(
    action: str,
    request: dict[str, Any],
    result: dict[str, Any],
    state: dict[str, Any],
) -> None:
    try:
        key = KEY_PATH.read_bytes().strip()
        seed_sha256 = hashlib.sha256(SEED_PATH.read_bytes()).hexdigest()
    except OSError as error:
        raise OperationError(f"sealed evidence material is unavailable: {error}") from error
    event: dict[str, Any] = {
        "version": 1,
        "sequence": next_sequence(),
        "action": action,
        "request": request,
        "result_sha256": digest(result),
        "seed_sha256": seed_sha256,
        "state_sha256": digest(state),
        "success": True,
    }
    event["signature"] = hmac.new(
        key,
        canonical(event),
        hashlib.sha256,
    ).hexdigest()
    try:
        with AUDIT.open("a", encoding="utf-8") as stream:
            stream.write(
                json.dumps(
                    event,
                    ensure_ascii=False,
                    sort_keys=True,
                    separators=(",", ":"),
                )
                + "\n"
            )
            stream.flush()
    except OSError as error:
        raise OperationError(f"cannot write sealed execution evidence: {error}") from error


def parser() -> argparse.ArgumentParser:
    root = argparse.ArgumentParser(
        prog="inventoryctl",
        description="Executable client for the sandboxed item register.",
    )
    commands = root.add_subparsers(dest="action", required=True)

    search = commands.add_parser(
        "search",
        help="search by exact item name and exact location",
    )
    search.add_argument("--name", required=True, help="literal item name")
    search.add_argument("--location", required=True, help="literal item location")

    get = commands.add_parser("get", help="retrieve one complete item record")
    get.add_argument("--id", required=True, help="stable item ID returned by search")

    commands.add_parser("list", help="list all item summaries")

    availability = commands.add_parser(
        "availability",
        help="check current quantity and status",
    )
    availability.add_argument("--id", required=True, help="stable item ID")

    update = commands.add_parser("update", help="change an item's status")
    update.add_argument("--id", required=True, help="stable item ID")
    update.add_argument("--status", required=True, help="replacement status")

    notify = commands.add_parser("notify", help="send an item notification")
    notify.add_argument("--id", required=True, help="stable item ID")
    notify.add_argument("--message", required=True, help="notification message")
    return root


def main() -> int:
    arguments = parser().parse_args()
    try:
        records = load_seed()
        RUNTIME.mkdir(mode=0o700, parents=True, exist_ok=True)
        with LOCK.open("a+b") as lock_stream:
            fcntl.flock(lock_stream.fileno(), fcntl.LOCK_EX)
            connection = connect(records)
            try:
                request, result = perform(connection, arguments.action, arguments)
                connection.commit()
                state = logical_state(connection)
                append_evidence(arguments.action, request, result, state)
            finally:
                connection.close()
        sys.stdout.write(
            json.dumps(
                result,
                ensure_ascii=False,
                sort_keys=True,
                separators=(",", ":"),
            )
            + "\n"
        )
        return 0
    except (OperationError, sqlite3.Error) as error:
        print(f"inventoryctl: {error}", file=sys.stderr)
        return 2


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