#!/usr/bin/env python3
"""Executable client for the sandboxed insurance claims export."""

from __future__ import annotations

import argparse
import fcntl
import hashlib
import json
from pathlib import Path
import shutil
import sys
import xml.etree.ElementTree as ET


ROOT = Path(__file__).resolve().parent
CANONICAL_EXPORT = ROOT / "data" / "claims.xml"
RUNTIME_DIR = ROOT / ".claim-runtime"
RUNTIME_EXPORT = RUNTIME_DIR / "claims.xml"
AUDIT_LOG = ROOT / "audit.log"


def emit(value: object) -> str:
    encoded = json.dumps(value, ensure_ascii=False, separators=(",", ":"))
    print(encoded)
    return encoded


def active_export(*, write: bool = False) -> Path:
    if write:
        RUNTIME_DIR.mkdir(exist_ok=True)
        if not RUNTIME_EXPORT.exists():
            shutil.copy2(CANONICAL_EXPORT, RUNTIME_EXPORT)
        return RUNTIME_EXPORT
    return RUNTIME_EXPORT if RUNTIME_EXPORT.exists() else CANONICAL_EXPORT


def load_claims(*, write: bool = False) -> tuple[ET.ElementTree, list[ET.Element]]:
    tree = ET.parse(active_export(write=write))
    return tree, list(tree.getroot().findall("claim"))


def record_for(claim: ET.Element) -> dict[str, str]:
    record = {"id": claim.attrib["id"]}
    for field in ("name", "location", "status", "related"):
        value = claim.findtext(field)
        if value is None:
            raise ValueError(f"claim {record['id']} is missing {field}")
        record[field] = value
    return record


def append_audit(event: dict[str, object]) -> None:
    encoded = json.dumps(event, sort_keys=True, separators=(",", ":")) + "\n"
    with AUDIT_LOG.open("a", encoding="utf-8") as stream:
        fcntl.flock(stream.fileno(), fcntl.LOCK_EX)
        stream.write(encoded)
        stream.flush()
        fcntl.flock(stream.fileno(), fcntl.LOCK_UN)


def get_claim(claim_id: str) -> int:
    _, claims = load_claims()
    claim = next((item for item in claims if item.attrib.get("id") == claim_id), None)
    record = record_for(claim) if claim is not None else None
    encoded = json.dumps(record, ensure_ascii=False, separators=(",", ":"))
    append_audit(
        {
            "found": record is not None,
            "id": claim_id,
            "operation": "get",
            "record_digest": hashlib.sha256(encoded.encode("utf-8")).hexdigest(),
        }
    )
    emit(record)
    return 0 if record is not None else 4


def search_claims(query: str) -> int:
    _, claims = load_claims()
    needle = query.casefold()
    matches = []
    for claim in claims:
        record = record_for(claim)
        searchable = " ".join(record.values()).casefold()
        if needle in searchable:
            matches.append(
                {
                    "id": record["id"],
                    "name": record["name"],
                    "location": record["location"],
                }
            )
    append_audit(
        {
            "match_count": len(matches),
            "operation": "search",
            "query_digest": hashlib.sha256(query.encode("utf-8")).hexdigest(),
        }
    )
    emit({"match_count": len(matches), "matches": matches})
    return 0


def update_claim(claim_id: str, field: str, value: str) -> int:
    tree, claims = load_claims(write=True)
    claim = next((item for item in claims if item.attrib.get("id") == claim_id), None)
    changed = claim is not None
    if claim is not None:
        claim.find(field).text = value
        tree.write(RUNTIME_EXPORT, encoding="utf-8", xml_declaration=True)
    append_audit(
        {
            "changed": changed,
            "field": field,
            "id": claim_id,
            "operation": "update",
            "value_digest": hashlib.sha256(value.encode("utf-8")).hexdigest(),
        }
    )
    emit({"changed": changed, "id": claim_id})
    return 0 if changed else 4


def parser() -> argparse.ArgumentParser:
    result = argparse.ArgumentParser(
        prog="claimctl",
        description="Query the sandboxed insurance claims export.",
    )
    commands = result.add_subparsers(dest="operation", required=True)

    get_parser = commands.add_parser("get", help="retrieve one full record by claim ID")
    get_parser.add_argument("claim_id", metavar="CLAIM_ID")

    search_parser = commands.add_parser("search", help="search claim summaries")
    search_parser.add_argument("query", metavar="QUERY")

    update_parser = commands.add_parser("update", help="update one claim field")
    update_parser.add_argument("claim_id", metavar="CLAIM_ID")
    update_parser.add_argument(
        "field",
        choices=("name", "location", "status", "related"),
    )
    update_parser.add_argument("value", metavar="VALUE")
    return result


def main() -> int:
    args = parser().parse_args()
    if args.operation == "get":
        return get_claim(args.claim_id)
    if args.operation == "search":
        return search_claims(args.query)
    if args.operation == "update":
        return update_claim(args.claim_id, args.field, args.value)
    raise AssertionError(args.operation)


if __name__ == "__main__":
    try:
        raise SystemExit(main())
    except (ET.ParseError, OSError, ValueError) as error:
        print(f"claimctl: {error}", file=sys.stderr)
        raise SystemExit(2)
