#!/usr/bin/env python3
"""Command-line interface to the task's disposable shipment database."""

from __future__ import annotations

import argparse
import fcntl
import json
import os
from pathlib import Path
import sqlite3
import sys
import time
from typing import Any


ROOT = Path(__file__).resolve().parent
STATE = ROOT / ".state"
DATABASE = STATE / "shipments.sqlite3"
HISTORY = STATE / "operations.jsonl"
HELP_MARKER = STATE / "help-used"
READ_DELAY_SECONDS = 0.50


def parser() -> argparse.ArgumentParser:
    command = argparse.ArgumentParser(
        prog="shipmentctl",
        description="Query or manage the local shipment registry.",
    )
    subcommands = command.add_subparsers(dest="operation", required=True)

    search = subcommands.add_parser("search", help="find shipments by exact name and city")
    search.add_argument("--name", required=True)
    search.add_argument("--city", required=True)

    get = subcommands.add_parser("get", help="retrieve one shipment by ID")
    get.add_argument("--id", required=True)

    create = subcommands.add_parser("create", help="create a shipment")
    create.add_argument("--id", required=True)
    create.add_argument("--name", required=True)
    create.add_argument("--city", required=True)
    create.add_argument("--status", required=True)

    update = subcommands.add_parser("update", help="change a shipment status")
    update.add_argument("--id", required=True)
    update.add_argument("--status", required=True)

    cancel = subcommands.add_parser("cancel", help="mark a shipment canceled")
    cancel.add_argument("--id", required=True)

    notify = subcommands.add_parser("notify", help="record a shipment notification")
    notify.add_argument("--id", required=True)
    notify.add_argument("--message", required=True)
    return command


def row_dict(row: sqlite3.Row) -> dict[str, str]:
    return {key: row[key] for key in ("id", "name", "city", "status")}


def append_history(
    operation: str,
    arguments: dict[str, str],
    result: Any,
    started_ns: int,
    ended_ns: int,
) -> None:
    event = {
        "operation": operation,
        "arguments": arguments,
        "result": result,
        "pid": os.getpid(),
        "parent_pid": os.getppid(),
        "process_group": os.getpgrp(),
        "started_ns": started_ns,
        "ended_ns": ended_ns,
    }
    encoded = json.dumps(event, sort_keys=True, separators=(",", ":")) + "\n"
    with HISTORY.open("a", encoding="utf-8") as stream:
        fcntl.flock(stream.fileno(), fcntl.LOCK_EX)
        stream.write(encoded)
        stream.flush()
        os.fsync(stream.fileno())
        fcntl.flock(stream.fileno(), fcntl.LOCK_UN)


def main() -> int:
    if sys.argv[1:] == ["--help"]:
        HELP_MARKER.touch()
    args = parser().parse_args()
    if not DATABASE.is_file():
        print("shipmentctl: local state has not been prepared", file=sys.stderr)
        return 69

    arguments = {
        key: str(value)
        for key, value in vars(args).items()
        if key != "operation" and value is not None
    }
    started_ns = time.monotonic_ns()
    connection = sqlite3.connect(DATABASE, timeout=5)
    connection.row_factory = sqlite3.Row
    try:
        if args.operation == "search":
            rows = connection.execute(
                "SELECT id, name, city FROM shipments "
                "WHERE name = ? AND city = ? ORDER BY id",
                (args.name, args.city),
            ).fetchall()
            result: Any = {
                "matches": [
                    {key: row[key] for key in ("id", "name", "city")} for row in rows
                ]
            }
            time.sleep(READ_DELAY_SECONDS)
        elif args.operation == "get":
            row = connection.execute(
                "SELECT id, name, city, status FROM shipments WHERE id = ?",
                (args.id,),
            ).fetchone()
            if row is None:
                result = {"record": None}
            else:
                result = {"record": row_dict(row)}
            time.sleep(READ_DELAY_SECONDS)
        elif args.operation == "create":
            connection.execute(
                "INSERT INTO shipments(id, name, city, status) VALUES (?, ?, ?, ?)",
                (args.id, args.name, args.city, args.status),
            )
            connection.commit()
            result = {"created": args.id}
        elif args.operation == "update":
            cursor = connection.execute(
                "UPDATE shipments SET status = ? WHERE id = ?",
                (args.status, args.id),
            )
            connection.commit()
            result = {"updated": cursor.rowcount}
        elif args.operation == "cancel":
            cursor = connection.execute(
                "UPDATE shipments SET status = 'canceled' WHERE id = ?",
                (args.id,),
            )
            connection.commit()
            result = {"canceled": cursor.rowcount}
        else:
            connection.execute(
                "INSERT INTO notifications(shipment_id, message) VALUES (?, ?)",
                (args.id, args.message),
            )
            connection.commit()
            result = {"notified": args.id}
    except sqlite3.Error as error:
        result = {"error": str(error)}
        ended_ns = time.monotonic_ns()
        append_history(args.operation, arguments, result, started_ns, ended_ns)
        print(json.dumps(result, sort_keys=True), file=sys.stderr)
        return 1
    finally:
        connection.close()

    ended_ns = time.monotonic_ns()
    append_history(args.operation, arguments, result, started_ns, ended_ns)
    print(json.dumps(result, sort_keys=True, separators=(",", ":")))
    return 0


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