# Copyright (c) VOLVICON.
#
# This file is provided solely for educational purposes in connection with
# the Volvicon software and related documentation.
#
# You may use and modify this file for personal or internal purposes when
# working with Volvicon products. Any reproduction, distribution, or use
# of this file outside the Volvicon ecosystem is strictly prohibited
# without prior written authorization from VOLVICON.

"""
Operation Log Operations Tutorial.

This tutorial demonstrates how to read the history of what happened in the
application: which actions were recorded, what each one reports, and how to
write that history to a file.

The service is read only. There is no way through it to delete, clear, edit or
rewrite anything the application recorded, and the calls you make through it
are not themselves added to the history.

Topics covered:
- Reporting what the history can and cannot say about itself
- Opening a listing over one recorded history and reading it a page at a time
- Reading the fields of one listed action
- Narrowing a listing by view, category, text and object name
- Noticing that actions were recorded after a listing was opened
- Writing a whole history, or only the listed actions, to a file

Prerequisites:
- Volvicon application must be running
- A valid license must be active
- Recording must be switched on; get_status() reports whether it is
"""

import os
import tempfile

# Import the Scripting API module
import ScriptingApi as api

# Create Application instance and get the operation log operations
app = api.Application()
log_operations = app.get_operation_log_operations()

# Write the example files next to the system temporary directory
output_directory = tempfile.gettempdir()

# =============================================================================
# What the History Can Say About Itself
# =============================================================================
# get_status() answers before anything is listed. It reports whether actions are
# being recorded at all, how much is held, and each way history can be missing.
# The four losses are separate questions: a stored history that could not be read
# is not the same thing as one that was never written.

status = log_operations.get_status()

print("Operation log status:")
print(f"  Recording: {status.recording}")
print(f"  Session: {status.session_id}")
print(f"  Project history: {status.project_log_id}")
print(f"  Records held for this session: {status.session_event_count}")
print(f"  Records held for the open project: {status.project_event_count}")
print(f"  Unreadable stored history kept: {status.retained_unreadable_project_log}")
print(f"  Project history truncated: {status.project_history_truncated}")
print(f"  Oldest session records dropped: {status.evicted_session_event_count}")
print(f"  Dropped count is known: {status.evicted_session_event_count_known}")

# degraded_reason is a message to show, not a code to compare against. Its
# wording can change; the flag beside it is what a script tests.
if status.degraded:
    print(f"  Recording problem: {status.degraded_reason}")

if not status.recording:
    print("Recording is off, so the listings below will be empty.")

# =============================================================================
# Opening a Listing
# =============================================================================
# A listing is a snapshot of one recorded history, taken when it is opened, so
# the pages you read afterwards agree with one another.
#
# Choose which history to list:
#   api.LogSource.ProjectHistory - what the open project carries and what a
#                                  project file would save, ordered by project
#                                  sequence
#   api.LogSource.ProjectSession - what this session recorded for the open
#                                  project, including session-only actions
#   api.LogSource.SessionTrace   - everything this process recorded, including a
#                                  project that was closed
#
# The three are ordered by different fields and are never mixed, so a call names
# exactly one of them.
#
# Only one listing is open at a time. Opening another closes the previous one,
# and so does an open that fails, so keep the token you are working with and
# open a new listing rather than returning to an old one.

# A filter left at its defaults narrows nothing beyond the view the Operation Log
# panel opens with, which is what the next section changes.
default_view = api.LogFilter()

listing = log_operations.open_listing(api.LogSource.SessionTrace, default_view)

print("\nListing:")
print(f"  Token: {listing.token}")
print(f"  Actions the history holds: {listing.total_actions}")
print(f"  Actions this listing walks: {listing.matched_actions}")
print(f"  Ordered by: {listing.ordered_by}")

# =============================================================================
# Reading a Page at a Time
# =============================================================================
# list_actions() takes the token, the first action of the page counted from
# zero, and how many to read. Reading past the end returns an empty list rather
# than raising, which is how a loop knows it has finished.
#
# A request larger than the supported maximum is served at that maximum, so
# continue from the next offset instead of asking for everything at once.

page_size = 50
offset = 0
all_actions = []

while True:
    page = log_operations.list_actions(listing.token, offset, page_size)
    if not page:
        break
    all_actions.extend(page)
    offset += len(page)

print(f"\nRead {len(all_actions)} actions in pages of {page_size}.")

# =============================================================================
# What One Listed Action Says
# =============================================================================
# Each entry is one row of the Operation Log: what the action was, what it
# worked on, and how it ended. The enumerated fields cross as text, so they can
# be printed and compared directly.

for entry in all_actions[:5]:
    print(f"\n  {entry.utc}  {entry.label}")
    print(f"    Operation key: {entry.operation_key}")
    print(f"    Category: {entry.category}")
    print(f"    Objects: {entry.objects}")
    print(f"    Started by: {entry.source}")
    print(f"    Outcome: {entry.outcome}")
    print(f"    Kept by the project: {entry.persistence}")
    print(f"    Undo state: {entry.undo_availability}")
    print(f"    Duration: {entry.duration_ms} ms")
    print(f"    Changed the project: {entry.change}")
    print(f"    Wrote a file: {entry.deliverable}")
    print(f"    Reported a result: {entry.finished}")

    if entry.result_summary:
        print(f"    Result: {entry.result_summary}")
    if entry.diagnostic_text:
        print(f"    Why it did not fully succeed: {entry.diagnostic_text}")

# An action that has not reported a result yet is still running, or was
# interrupted before it could report one.
unfinished = [entry for entry in all_actions if not entry.finished]
print(f"\nActions with no reported result: {len(unfinished)}")

# =============================================================================
# Narrowing a Listing
# =============================================================================
# api.LogFilter() carries the same controls as the Operation Log panel. Every
# field left at its default narrows nothing, so a filter you do not change opens
# the same view the panel opens with.
#
# Available views:
#   api.LogPreset.Highlights     - the default: actions that changed the project,
#                                  wrote a file, or did not fully succeed
#   api.LogPreset.AllActions     - everything recorded, including the session
#                                  boundaries and any row an earlier version
#                                  recorded and this one no longer does
#   api.LogPreset.ScriptCalls    - actions a script performed
#   api.LogPreset.NeedsAttention - actions that failed, were cancelled, were
#                                  interrupted or partly succeeded
#   api.LogPreset.UndoRedo       - actions that were undone or redone

# Everything the session recorded, not only the default view
everything = api.LogFilter()
everything.preset = api.LogPreset.AllActions

listing = log_operations.open_listing(api.LogSource.SessionTrace, everything)
print(f"\nAll actions: {listing.matched_actions} of {listing.total_actions}")

# Only what did not fully succeed
attention = api.LogFilter()
attention.preset = api.LogPreset.NeedsAttention

listing = log_operations.open_listing(api.LogSource.SessionTrace, attention)
print(f"Actions needing attention: {listing.matched_actions}")

# One catalogued category, such as "Surface", "Mask", "Volume" or "AI Segmentation".
# Changing how the scene looks is not recorded, so there is no category for it.
by_category = api.LogFilter()
by_category.preset = api.LogPreset.AllActions
by_category.category = "Surface"

listing = log_operations.open_listing(api.LogSource.SessionTrace, by_category)
print(f"Surface actions: {listing.matched_actions}")

# Free text, matched literally and case-insensitively over what the action shows
by_text = api.LogFilter()
by_text.preset = api.LogPreset.AllActions
by_text.text = "smoothing"

listing = log_operations.open_listing(api.LogSource.SessionTrace, by_text)
print(f"Actions mentioning 'smoothing': {listing.matched_actions}")

# One or more objects, named as the history captured them
by_object = api.LogFilter()
by_object.preset = api.LogPreset.AllActions
by_object.object_names = ["Femur"]

listing = log_operations.open_listing(api.LogSource.SessionTrace, by_object)
print(f"Actions on 'Femur': {listing.matched_actions}")

# Over a long history, a filter that matches more actions than one listing holds
# is refused rather than served short, and the message says how many it matched
# so the filter can be narrowed. Handle it where a broad filter is possible.
try:
    listing = log_operations.open_listing(api.LogSource.SessionTrace, everything)
except RuntimeError as error:
    print(f"Listing refused, narrow the filter: {error}")
    listing = log_operations.open_listing(api.LogSource.SessionTrace, by_category)

# =============================================================================
# Noticing That the History Moved
# =============================================================================
# A listing keeps answering for the moment it was taken, so a page never changes
# underneath the caller. has_newer_actions() reports whether anything was
# recorded since, which is how a script decides to open the listing again.

listing = log_operations.open_listing(api.LogSource.SessionTrace, everything)

if log_operations.has_newer_actions(listing.token):
    print("\nThe history moved on; opening the listing again would show more.")
    listing = log_operations.open_listing(api.LogSource.SessionTrace, everything)

# Two changes end a listing instead of being reported: the oldest records being
# dropped, and the open project being replaced. A call against a listing that is
# no longer valid raises rather than returning a wrong page.
# try:
#     log_operations.list_actions(listing.token, 0, 10)
# except RuntimeError as error:
#     print(f"Open the listing again: {error}")

# =============================================================================
# Saving a History to a File
# =============================================================================
# export_to_disk() writes a whole recorded history. The file states which history
# it holds and whether anything is missing from it.
#
# Two formats:
#   api.LogExportFormat.Jsonl - one JSON object per line, carrying every field of
#                               every selected record
#   api.LogExportFormat.Csv   - a flat summary for a spreadsheet, one line per
#                               listed action

jsonl_path = os.path.join(output_directory, "volvicon_history.jsonl")
csv_path = os.path.join(output_directory, "volvicon_history.csv")

log_operations.export_to_disk(api.LogSource.ProjectHistory, api.LogExportFormat.Jsonl, jsonl_path)
print(f"\nProject history written to: {jsonl_path}")

log_operations.export_to_disk(api.LogSource.SessionTrace, api.LogExportFormat.Csv, csv_path)
print(f"Session trace written to: {csv_path}")

# =============================================================================
# Saving Only the Listed Actions
# =============================================================================
# export_listing_to_disk() writes the actions of an open listing rather than the
# whole history, so a filter decides what the file holds.
#
# If a listed action is no longer held by the history it came from, the whole
# export is refused rather than a file being written that is quietly missing it.

listing = log_operations.open_listing(api.LogSource.SessionTrace, attention)
attention_path = os.path.join(output_directory, "volvicon_needs_attention.csv")

if listing.matched_actions > 0:
    log_operations.export_listing_to_disk(listing.token, api.LogExportFormat.Csv, attention_path)
    print(f"{listing.matched_actions} actions needing attention written to: {attention_path}")
else:
    print("Nothing needed attention in this session.")

# =============================================================================
# Closing a Listing
# =============================================================================
# Closing releases what the listing was holding. A listing is also closed by
# opening another one, and when the application closes, so a script that finishes
# without closing leaves nothing behind. Closing a token that names no listing
# does nothing.

log_operations.close_listing(listing.token)
print("\nListing closed.")
