Writing Scripts with the Seal Module

The Seal Platform allows you to write and execute your own custom Python code within the platform, enabling powerful custom solutions and automations.

If this is your first time using Python, then there are great reference materials online such as: https://docs.python.org/3.10/tutorial/

What if I don't know how to code?

Not to worry - Seal comes with a built in AI Copilot to write code for you. Navigate to the 'Copilot' tab on the right hand side, and start chatting with the AI to describe what you need.

The AI agent has access to your current code, so you can ask it to help you debug specific issues or add to the existing code.

Once you're happy with the code, click on 'Accept code' to use it in your Script.

Note that the generated code may not fully be fit for your purpose - we recommend checking over the results.

The Seal Module

The Seal module is a Python package for interacting programatically with the Seal platform.

It is automatically available in every Script's Python environment - you don't need to import it manually.

The Seal module consists of a collection of methods on the seal object:

Getting entities

seal.get_entity(entity_id, ?version)
seal.get_entity(ref={"id: "...", "version": "..."})

Returns the entire json blob of data for that entity. The entity id can be copied from the entity's URL in Seal - it's the final section of the url, after the entity title. Just passing an entity_id returns the latest version or draft of the entity.

The entity id can be copied from the entity's URL in Seal - it's the final section of the url, after the entity title.

To get a specific version of an entity, include the version as a second argument. You can also pass an entity ref object instead.

Since it returns the whole json blob for the entity, you can get the entity's fields via entity["fields"] , or a specific field via entity["fields"]["fieldName"] .

seal.get_entity_active_version(entity_id)

Gets the data for the active version of the given entity. If no active version is found (ie the entity has never been published), an error will be thrown.

seal.get_containing_entity()

Gets the entity the script card is embedded in.

seal.get_entities_by_title(title)

Gets (non-archived) entities by their title (string). Note that exact titles are matched by default. Since multiple entities can share the same title, this returns an array. You can also search for titles containing the search string with exact=False.

seal.search_entities(query_config)

Gets entities by searching with a query config object (dict). See the API documentation for more details on the query config schema.

Making an entity editable

seal.make_entity_editable(entity_id)

Used to make an entity editable (ie create a new draft). Returns the entire json blob of data for the new draft entity.

Reverting entity drafts

seal.revert_entities(entity_ids)

Revert multiple entity drafts to their previous published versions. All entities must be in EDITABLE status and have a previous version. Up to 500 entities can be reverted at once.

Parameters:

  • entity_ids: List of entity IDs to revert (maximum 500)

Returns an array of the reverted entities.

seal.revert_containing_entity()

Revert the entity the script is embedded in to its previous published version. The entity must be in EDITABLE status and have a previous version.

Returns an array containing the reverted entity.

Validating entities

Scripts can be used to run checks before an entity is published. A script can be added as a check on a type or template for its templates or instances.

seal.get_validating_entity()

Gets the entity a script is validating.

When a validation script is run, it is classed as passing if no errors are thrown. Any of the following methods will throw an error if the assertion is not met:

Method
Checks that

validation.assertEqual(a, b)

a == b

validation.assertNotEqual(a, b)

a != b

validation.assertTrue(x)

bool(x) is True

validation.assertFalse(x)

bool(x) is False

validation.assertIs(a, b)

a is b

validation.assertIsNot(a, b)

a is not b

validation.assertIsNone(x)

x is None

validation.assertIsNotNone(x)

x is not None

validation.assertIn(a, b)

a in b

validation.assertNotIn(a, b)

a not in b

validation.assertIsInstance(a, b)

isinstance(a, b)

validation.assertNotIsInstance(a, b)

not isinstance(a, b)

User-defined errors can also be thrown to fail a validation script.

seal.throw_error(message)

Throws an error with a user-provided message.

Archiving an entity

seal.archive_entity(entity_id, archive)

Archives or unarchives an entity. The archive parameter should be a boolean: True to archive, False to recover. Returns the updated entity data.

seal.archive_entities(entity_ids)

Archives multiple entities. Must pass in an array of entity_id values. Returns the ids that were archived.

Converting an Instance to a Template

seal.convert_instance_to_template(entity_id)

Converts an instance entity into a template entity. The instance must be editable (a draft) to be converted. Returns the entire json blob of data for the new template entity.

Updating the status tag on an entity

seal.update_entity_status_tag(entity_id, status_tag)

Updates the status tag on an entity. The live entity must be in an editable state.

seal.update_upcoming_entity_status_tag(entity_id, status_tag)

Updates the upcoming status tag an entity will have when it is next published. The live entity must be in an editable state.

Getting the upcoming version info of an entity

seal.get_upcoming_version_info(entity_id)

Returns the upcoming version info of an entity. This includes the version and status tag.

Adding an entity to a change set

seal.add_entity_to_change_set(entity_id, change_set_index)

Used to add an entity to a pre-existing change set. The change_set_index can be found in the URL when on a change set page.

Returns information about the change set the given entity now belongs to. This information includes id, index, name, status and description.

Creating Change Sets

seal.create_change_set(entity_ids, ?change_set_name)

Creates a new change set containing the provided entities. Returns information about the created change set including id, index, name, status, description and entityRefs.

Getting tags

seal.get_tags_for_entity(entity_id)

Gets the tags on an entity.

seal.get_tags(tag)

Gets the tags on an entity the script is embedded in. Can only be run from a script card.

Adding tags

seal.add_tag_to_entity(entity_id, tag)

Adds a tag to an entity. If no tag matching the input tag is found in the organisation, a new tag will be created.

seal.add_tag(tag)

Adds a tag to the entity the script is embedded in. Can only be run from a script card.

Deleting tags

seal.delete_tag(tag_name)

Remove a tag by name from the entity within which the script is embedded.

seal.delete_tag_from_entity(entity_id, tag_name)

Remove a tag by name in any entity.

Setting assignees

seal.set_assignees(entity_id, assignees)

Set the assignees for an entity. The assignees parameter should be a list of strings where each item is either an email address or a user id.

Getting workflow tasks

seal.get_workflow_tasks(entity_id, ?entity_version)

Gets all workflow tasks associated with a workflow entity. Returns a list of dictionaries, each representing a full workflow task object. Each task includes a completedAt timestamp if the task has been completed, or null if the task is not completed.

seal.get_workflow_tasks_for_containing_entity()

Gets all workflow tasks for the entity that a script is embedded in. Can only be run from a script card. Each task includes a completedAt timestamp if the task has been completed.

Getting workflows that reference an entity

seal.get_workflows_referencing(entity_id, ?entity_version)

Gets all workflow entities that have tasks referencing the specified entity either as a template or instance. Returns an array of full workflow entities. If no entity version is provided, workflows referencing the draft entity are returned.

seal.get_containing_entity_workflows_referencing()

Gets all workflow entities that have tasks referencing the entity the script is embedded in. Can only be run from a script card. Uses the specific version of the containing entity.

Getting change sets

seal.get_change_set_for_entity(entity_id, ?version)
seal.get_change_set_for_entity(ref={"id": "...", "version": "..."})

Returns information about the change set the given entity belongs to. This information includes id, index, name, status and description. By default, we find the change set for the latest draft of the entity (if it exists).

To find the change set that a specific version of an entity belongs to, include the version as a second argument. You can also pass an entity ref object instead.

seal.get_change_set_for_containing_entity()

Gets the change set for the entity that a script is embedded in. Can only be run from a script card.

Adding fields

seal.add_field(field_name, field_type, value, allow_multiple, select_options, multi_line, formula_expression)

Add a new field to the embedded-in entity.

For specific field types, refer to this specific section in the Scripts docs.

seal.add_field_to_entity(entity_id, field_name, field_type, value, allow_multiple, select_options, multi_line, formula_expression)

Add a new field to any entity.

Updating fields

seal.update_field_value(field_name, field_value)

Update a field value in the entity the script is embedded in. Can only be run from a script card.

seal.update_field_value_in_entity(entity_id, field_name, field_value)

Update a field value in any entity.

Deleting fields

seal.delete_field(field_name)

Remove a field from the embedded-in entity. Note that the field's data will also be removed.

seal.delete_field_in_entity(entity_id, field_name)

Delete field in any entity.

Renaming fields

seal.rename_field(field_name, new_field_name)

Rename a field in the entity the script is embedded in. Can only be run from a script card.

seal.rename_field_in_entity(entity_id, field_name, new_field_name)

Rename a field in any entity.

Updating properties

seal.update_entity_title(entity_id, title, ?overwrite_computed_title)

Update the title of any entity. If overwrite_computed_title is set to True and the instance has a computed title, this will be overwritten.

seal.update_containing_entity_title(title, ?overwrite_computed_title)

Update the title of the entity the script is embedded in. Can only be run from a script card.

Getting out of spec fields

seal.get_out_of_spec_fields(entity_id, ?version)
seal.get_out_of_spec_fields(ref={"id: "...", "version": "..."})

Gets the out of spec fields for the provided entity. By default, the latest version or draft of the entity is used.

To view the out of spec fields from a specific version of an entity, include the version as a second argument. You can also pass an entity ref object instead.

Handling files

seal.download_file(file_entity_id, ?version)

The file is downloaded to the local filesystem of the virtual machine instance. The value returned is the local file path as a string.

seal.upload_file(file_path, file_name, type_title, ?extract_fields_with_ai)

Uploads a file into a new entity. The provided type must have 'File' content type. Returns the entire json blob of data of the new entity.

AI data extraction

seal.extract_data_from_file(file_entity_id, version, ?=prompt)

Extract data from a file entity. See Parameters below:

  • file_entity_id: ID of the file entity to extract data from

  • version: (Optional) Specific version of the file entity

  • prompt: (Optional, keyword-only) Custom prompt to guide the data extraction

Returns: json Example:

data = seal.extract_data_from_file(
    "9351e027-d26c-4769-a395-507f4f148cb4",
     prompt="Extract all temperature readings by day"
)

Submitting Data

seal.submit_to_instance_submission_field(
   "field_name",
   field_values_df=pd.DataFrame([{ "field": "value", "Title": 'title'}]),
   number_of_empty_instances=3
   template_ref={"id": "...", "version": "..."}
)

Submit instances to a submission field in the surrounding entity.

Submit one or more instances to an instance submission field, with optional initial field values in a dataframe. If a column is called 'title' or 'Title', it will set the instances' titles of setting the values of a field called 'title' or 'Title').

You can optionally specify a number of empty instances to create.

If the submission field refers to a type, a template_ref to a template of the type must be provided.

You can also submit data to a submission table in any entity:

seal.submit_to_instance_submission_field_in_entity(entity_id, ...the same)

Submission Placeholder Columns

Placeholder columns are additional fields that can be added to all rows in a submission table. These columns are automatically added to existing instances in the submission field.

seal.add_placeholder_column_to_submission_field(field_name, column_name, column_type)

Add a placeholder column to an instance submission field in the entity the script is embedded in. Can only be run from a script card.

seal.add_placeholder_column_to_submission_field_in_entity(entity_id, field_name, column_name, column_type)

Add a placeholder column to an instance submission field in any entity.

seal.update_placeholder_column_config(field_name, column_name, *, allow_multiple=None, select_options=None, multi_line=None, format=None)

Update the configuration of a placeholder column in the entity the script is embedded in. Configuration parameters are keyword-only and optional. Can only be run from a script card.

seal.update_placeholder_column_config_in_entity(entity_id, field_name, column_name, *, allow_multiple=None, select_options=None, multi_line=None, format=None)

Update the configuration of a placeholder column in any entity. Configuration parameters are keyword-only and optional.

seal.remove_placeholder_column_from_submission_field(field_name, column_name)

Remove a placeholder column from an instance submission field in the entity the script is embedded in. Can only be run from a script card.

seal.remove_placeholder_column_from_submission_field_in_entity(entity_id, field_name, column_name)

Remove a placeholder column from an instance submission field in any entity.

Creating Instances

seal.create_instance_from_template(
  template_id,
  ?version,
  field_values={ "field": "value", ... },
  title='title'
)
seal.create_instance_from_template(
  ref={"id: "...", "version": "..."},
  field_values={ "field": "value", ... },
  title='title'
)

Create an instance from a template. By default, instances are created from the latest published version of the template.

To create an instance from a specific version of a template, include the version as a second argument. You can also pass an entity ref object instead.

Parameters:

  • template_id: ID of the template to create an instance from

  • version: (Optional, keyword-only) Specific version of the template

  • field_values: (Optional, keyword-only) Initial field values for the instance. The specified fields must exist on the template.

Similarly, you can also create test instances from a template:

seal.create_test_instance_from_template(
  template_id,
  version,
  field_values={ "field": "value", ... },
)

Creating Charts

chart = alt.Chart(data).mark_bar().encode(x="x", y="y")
chart_entity_id = seal.create_chart(chart, title="Sample Chart", type_title="Chart")

Generate a chart from an Altair chart. Returns the created chart entity_id as a string. See parameters below:\

  • chart: The Altair chart instance

  • title:The name for the chart

  • type_title: The name of the type to create a chart from. The content type must be Chart.

Running Scripts

seal.run_script(
  script_id,
  ?version
)
seal.run_script(
  ref={"id: "...", "version": "..."},
)

Run an entity that has content type Script code. By default, the latest version of the entity is run.

To run a specific version of a script entity, include the version as a second argument. You can also pass an entity ref object instead.

Parameters:

  • script_id: ID of the script to run

  • version: (Optional, keyword-only) Specific version of the script

seal.run_embedded_scripts(
  entity_id,
  ?version,
  ?card_ids=["..."]
)
seal.run_embedded_scripts(
  ref={"id: "...", "version": "..."},
  ?card_ids=["..."]
)

Parameters:

  • entity_id: ID of the entity with embedded scripts

  • version: (Optional, keyword-only) Specific version of the entity

  • card_ids: (Optional, keyword-only) List of specific card ids to run on the page

Run all action buttons and script cards embedded in an entity with page content. You can optionally pass in the card_ids argument to specify which scripts in particular should be run. These IDs can be found in the page content of the entity (see the Entity Schema for more details).

seal.get_live_backlinks(entity_id)

Returns the ids of all entities whose live data includes a reference to any version of the requested entity.

Getting trigger info

seal.get_trigger_info()

If the script is being run as a trigger, this method returns an object containing context about the trigger run: trigger_id and triggered_by_entity_id.

Importing packages

Seal comes with many common Python packages pre-installed. If there are other python packages you regularly require, please contact Seal support.

These packages are automatically imported in every Script, so don't need to be manually imported:

  • altair (also aliased as alt)

  • Chart (alias of altair.Chart)

  • pandas (also aliased as pd)

Available packages to import include:
  • altair

  • annotated-types

  • attrs

  • blinker

  • cachetools

  • certifi

  • cffi

  • charset-normalizer

  • click

  • cloudevents

  • contourpy

  • cryptography

  • cycler

  • deprecation

  • et-xmlfile

  • flask

  • fonttools

  • functions-framework

  • gcloud

  • google-api-core

  • google-auth

  • google-cloud-appengine-logging

  • google-cloud-audit-log

  • google-cloud-core

  • google-cloud-error-reporting

  • google-cloud-logging

  • google-cloud-storage

  • google-crc32c

  • google-resumable-media

  • googleapis-common-protos

  • grpc-google-iam-v1

  • grpcio

  • grpcio-status

  • gunicorn

  • httplib2

  • idna

  • itsdangerous

  • jinja2

  • joblib

  • jsonschema

  • jsonschema-specifications

  • jwcrypto

  • kiwisolver

  • lxml

  • markupsafe

  • matplotlib

  • numpy

  • oauth2client

  • openpyxl

  • packaging

  • pandas

  • pillow

  • proto-plus

  • protobuf

  • pyasn1

  • pyasn1-modules

  • pycorn

  • pycparser

  • pycryptodome

  • pydantic

  • pydantic-core

  • pyparsing

  • pyrebase4

  • python-dateutil

  • python-docx

  • python-jwt

  • pytz

  • referencing

  • requests

  • requests-toolbelt

  • rpds-py

  • rsa

  • scikit-learn

  • scipy

  • setuptools

  • six

  • tabulate

  • threadpoolctl

  • toolz

  • typing-extensions

  • urllib3

  • vl-convert-python

  • watchdog

  • werkzeug

  • xlrd

To temporarily install a package when running the script:

import subprocess
subprocess.check_output("pip install {name of package}".split())

Examples

Creating and Linking Charts

This example script would be embedded in an entity containing a SUBMISSION field, with your chart data, and a REFERENCE field to embed the generated chart in.

import pandas as pd
import altair as alt

# Name of the chart entity to create
CHART_TITLE = "Growth Rate"
# Your type entity with content type: 'Chart'
# Find or create the type from org setting -> types
CHART_TYPE_NAME = "Chart"
# The containing entity REFERENCE field to append the generated file entity to
REFERENCE_FIELD_NAME = "Charts"
# Submission field name containing your chart data
SUBMISSION_FIELD_NAME = "Data"
# Submission column name to plot on the X axis
SUBMISSION_X_COLUMN = "Time"
# Submission column name to plot on the Y axis
SUBMISSION_Y_COLUMN = "Growth"

# Extract data from submission field
entity = seal.get_containing_entity()
submission_field = entity["fields"].get(SUBMISSION_FIELD_NAME, {})
submitted_refs = submission_field.get("value", [])

# Collect data from submitted entities
data = []
for ref in submitted_refs:
    submitted_entity = seal.get_entity(ref=ref)
    fields = submitted_entity["fields"]

    timepoint = fields.get(SUBMISSION_X_COLUMN, {}).get("value")
    growth_rate = fields.get(SUBMISSION_Y_COLUMN, {}).get("value")

    if timepoint and growth_rate:
        data.append({SUBMISSION_X_COLUMN: timepoint, SUBMISSION_Y_COLUMN: growth_rate})

# Create DataFrame and chart
df = pd.DataFrame(data)
chart = alt.Chart(df).mark_line(point=True).encode(x=SUBMISSION_X_COLUMN, y=SUBMISSION_Y_COLUMN)

# Create chart entity and link to reference field
chart_id = seal.create_chart(chart, title=CHART_TITLE, type_title=CHART_TYPE)

existing_refs = entity["fields"].get(REFERENCE_FIELD_NAME, {}).get("value", []) or []
updated_refs = existing_refs + [{"id": chart_id, "version": None}]
seal.update_field_value(REFERENCE_FIELD_NAME, updated_refs)

Creating Labels

Create PDF labels from entity data with configurable layouts. This method requires the Label creation blueprint to be installed. To setup this up, see Creating labels blueprint.

seal.create_label_v1()

This script uses the ReportLab PDF Library to create labels with text, barcodes (Code128/Code39/QR), images, and geometric elements. It automatically operates in Preview Mode when embedded in a configuration entity (for testing) or Trigger Mode when triggered by entity updates (for automatic label generation). The configuration is stored in entities with layout elements defined in submission tables.

Complete Label Generation Script
import io
import os
import time

from reportlab.graphics.barcode import code39, code128, qr  # type: ignore
from reportlab.lib import colors  # type: ignore
from reportlab.lib.colors import black, blue, gray, red, white  # type: ignore
from reportlab.lib.units import cm, inch, mm  # type: ignore
from reportlab.pdfbase.pdfmetrics import stringWidth  # type: ignore
from reportlab.pdfgen import canvas  # type: ignore
from reportlab.platypus import Table, TableStyle  # type: ignore

UNIT_OBJECTS = {"cm": cm, "mm": mm, "inch": inch}

COLOR_MAP = {
    "Black": black,
    "Gray": gray,
    "Blue": blue,
    "Red": red,
    "White": white,
    "None": None,
}

DEFAULT_FONT_SIZE = 10
DEFAULT_FONT_NAME = "Helvetica"

# Entity properties support (copied from compute.ts)
ENTITY_PROPERTIES = [
    "CREATED_AT",
    "CREATED_BY",
    "EDITORS",
    "ID",
    "INDEX",
    "LAST_EDITED_AT",
    "LAST_EDITED_BY",
    "TEMPLATE_INDEX",
    "TITLE",
    "TYPE_INDEX",
    "SUBMITTED_FROM",
]


def is_entity_property(property_name):
    """Check if a property name is an entity property (case-insensitive)"""
    return property_name.upper() in ENTITY_PROPERTIES


def get_field_or_property_value(entity, column_name):
    """Get value from entity field or property (case-insensitive for properties)"""
    if is_entity_property(column_name):
        property_name = column_name.upper()
        if property_name == "TITLE":
            return entity.get("title", "")
        elif property_name == "ID":
            return entity.get("id", "")
        elif property_name == "CREATED_AT":
            return entity.get("createdAt", "")
        elif property_name == "CREATED_BY":
            return entity.get("createdBy", "")
        elif property_name == "EDITORS":
            return entity.get("editors", "")
        elif property_name == "INDEX":
            return entity.get("index", "")
        elif property_name == "LAST_EDITED_AT":
            return entity.get("lastEditedAt", "")
        elif property_name == "LAST_EDITED_BY":
            return entity.get("lastEditedBy", "")
        elif property_name == "TEMPLATE_INDEX":
            return entity.get("templateIndex", "")
        elif property_name == "TYPE_INDEX":
            return entity.get("typeIndex", "")
        elif property_name == "SUBMITTED_FROM":
            return entity.get("submittedFrom", "")
        else:
            # hard to maintain let's fallback to lowercase
            return entity.get(column_name.lower(), "")
    else:
        # Handle field values using existing function
        fields = entity.get("fields", {})
        return extract_field_value(fields, column_name, "")


def extract_field_value(field_dict, field_name, default_value):
    """Extract field value handling SELECT arrays and whitespace"""
    field_value = field_dict.get(field_name, {}).get("value", default_value)

    if isinstance(field_value, list) and field_value:
        result = field_value[0]
    elif isinstance(field_value, list):
        result = default_value
    else:
        result = field_value if field_value is not None else default_value

    return result


def collect_all_entity_references(entity_data, config):
    """Collect all entity references from REFERENCE and INSTANCE_SUBMISSION fields to batch fetch them"""
    entity_refs_to_fetch = set()

    # Look through all entity data for entity references
    for field_name, field_value in entity_data.items():
        if isinstance(field_value, list):
            for item in field_value:
                if isinstance(item, dict) and "id" in item:
                    entity_refs_to_fetch.add(item["id"])

    return list(entity_refs_to_fetch)


def collect_entity_references_from_entity(entity):
    """Collect entity references from an entity's REFERENCE and INSTANCE_SUBMISSION fields"""
    entity_refs_to_fetch = set()
    fields = entity.get("fields", {})

    for field_name, field_info in fields.items():
        field_type = field_info.get("type", "")
        field_value = field_info.get("value", "")

        # Look for REFERENCE and INSTANCE_SUBMISSION fields
        if field_type in ["REFERENCE", "INSTANCE_SUBMISSION"] and isinstance(
            field_value, list
        ):
            for ref in field_value:
                if isinstance(ref, dict) and "id" in ref:
                    entity_refs_to_fetch.add(ref["id"])

    return list(entity_refs_to_fetch)


def batch_fetch_entities(entity_ids, entity_cache):
    """Fetch multiple entities and add to cache"""
    if not entity_ids:
        return entity_cache

    for entity_id in entity_ids:
        if entity_id not in entity_cache:
            try:
                entity = seal.get_entity(entity_id=entity_id)
                entity_cache[entity_id] = entity
                entity_title = entity.get("title", "Untitled")
                print(f"Fetched entity: {entity_title}")
            except Exception as e:
                print(f"Failed to fetch entity {entity_id}: {e}")
                entity_cache[entity_id] = None

    return entity_cache


def find_config_for_template(config_template_id, target_template_id):
    """Find configuration instance(s) that support the target template ID"""
    if not config_template_id:
        return {"matches": [], "error": "No configuration template ID provided"}

    # Search for instances created from the configuration template
    search_query = {"filters": {"kind": ["INSTANCE"], "template": [config_template_id]}}

    try:
        config_instances = seal.search_entities(search_query)
        matching_configs = []

        for instance in config_instances:
            try:
                instance_entity = seal.get_entity(entity_id=instance["id"])
                instance_fields = instance_entity.get("fields", {})

                # Check the Templates reference field
                templates_refs = instance_fields.get("Templates", {}).get("value", [])

                if templates_refs and isinstance(templates_refs, list):
                    supported_template_ids = [
                        ref.get("id", "") for ref in templates_refs
                    ]

                    if target_template_id in supported_template_ids:
                        config_title = instance_entity.get(
                            "title", f"Untitled Config ({instance_entity['id']})"
                        )
                        matching_configs.append(
                            {"id": instance_entity["id"], "title": config_title}
                        )

            except Exception as e:
                print(f"Failed to process config instance {instance['id']}: {str(e)}")
                continue

        if len(matching_configs) == 0:
            return {
                "matches": [],
                "error": f"No configuration found for template '{target_template_id}'",
            }
        elif len(matching_configs) == 1:
            print(
                f"Found configuration '{matching_configs[0]['title']}' for template '{target_template_id}'"
            )
            return {"matches": matching_configs, "error": None}
        else:
            # Multiple matches - this is an error condition
            config_titles = [config["title"] for config in matching_configs]
            config_list = ", ".join(config_titles)
            print(
                f"WARNING: Found {len(matching_configs)} configurations for template "
                f"'{target_template_id}': {config_list}"
            )
            error_message = (
                f"Multiple configurations found for template '{target_template_id}': "
                f"{config_list}. Please delete unused configurations to avoid ambiguity."
            )
            return {
                "matches": matching_configs,
                "error": error_message,
            }

    except Exception as e:
        return {
            "matches": [],
            "error": f"Failed to search for configuration instances: {str(e)}",
        }


def main():
    """Main entry point with tri-mode detection"""
    try:
        config = load_script_configuration()

        try:
            trigger_info = seal.get_trigger_info()
            print("Running in TRIGGER MODE")
            run_trigger_mode(config, trigger_info.triggered_by_entity_id)
        except Exception as e:
            if "not called as a trigger" in str(e):
                # Determine if Preview or Manual mode based on containing entity
                mode_info = config.get("_mode_info", {})
                if mode_info.get("mode") == "preview":
                    print("Running in PREVIEW MODE")
                    run_preview_mode(config)
                elif mode_info.get("mode") == "manual":
                    print("Running in MANUAL MODE")
                    run_manual_mode(config, mode_info.get("containing_entity_id"))
                else:
                    raise Exception("Could not determine script mode")
            else:
                raise e

    except Exception as e:
        print(f"Script execution failed: {str(e)}")
        raise


def load_script_configuration():
    """Load configuration from referenced configuration entity"""
    script_entity_id = seal.entity_id

    script_entity = seal.get_entity(entity_id=script_entity_id)
    script_fields = script_entity.get("fields", {})

    # Determine which configuration entity to use
    config_entity = None
    config_field_name = None

    try:
        # Try to get trigger info to determine the triggering entity template
        trigger_info = seal.get_trigger_info()
        triggering_entity_id = trigger_info.triggered_by_entity_id
        triggering_entity = seal.get_entity(entity_id=triggering_entity_id)
        triggering_template_ref = triggering_entity.get("sourceInfo", {}).get(
            "template", {}
        )
        triggering_template_id = triggering_template_ref.get("id", "")

        if not triggering_template_id:
            raise Exception("Triggering entity has no template ID")

        # Get the Label Configuration Template reference from script entity
        script_entity_id = seal.entity_id
        script_entity = seal.get_entity(entity_id=script_entity_id)
        script_fields = script_entity.get("fields", {})

        config_template_refs = script_fields.get(
            "Label Configuration Template", {}
        ).get("value", [])

        if (
            not config_template_refs
            or not isinstance(config_template_refs, list)
            or len(config_template_refs) == 0
        ):
            raise Exception(
                "No Label Configuration Template found in script. Please add a "
                "'Label Configuration Template' reference field to the script entity."
            )

        config_template_id = config_template_refs[0].get("id", "")

        if not config_template_id:
            raise Exception("Label Configuration Template reference is invalid")

        # Search for configuration instance that supports this template
        config_result = find_config_for_template(
            config_template_id, triggering_template_id
        )

        if config_result["error"]:
            error_details = config_result["error"]
            raise Exception(
                f"Configuration lookup failed for template '{triggering_template_id}': "
                f"{error_details}\n"
                f"Please ensure you have exactly one configuration instance from the "
                f"Label Configuration Template with '{triggering_template_id}' in the "
                f"'Templates' field."
            )

        config_entity_id = config_result["matches"][0]["id"]
        config_field_name = f"Trigger Mode (template {triggering_template_id})"

        config_entity = seal.get_entity(entity_id=config_entity_id)

    except Exception as e:
        try:
            containing_entity = seal.get_containing_entity()
            containing_fields = containing_entity.get("fields", {})

            # Check if containing entity has "Label Elements" field to determine mode
            if "Label Elements" in containing_fields:
                # Preview mode: containing entity is a configuration entity
                config_entity = containing_entity
                config_field_name = "Preview Mode"
                mode = "preview"
            else:
                # Manual mode: containing entity is a regular entity that wants a label
                config_entity = None  # Will be determined from Label Configuration Template instances
                config_field_name = "Manual Mode"
                mode = "manual"
                containing_entity_id = containing_entity["id"]

            if mode == "manual":
                # Manual mode: find configuration using containing entity's template
                containing_template_ref = containing_entity.get("sourceInfo", {}).get(
                    "template", {}
                )
                containing_template_id = containing_template_ref.get("id", "")

                if not containing_template_id:
                    raise Exception("Manual mode: Containing entity has no template ID")

                # Get Label Configuration Template from script entity to find matching config
                script_entity_id = seal.entity_id
                script_entity = seal.get_entity(entity_id=script_entity_id)
                script_fields = script_entity.get("fields", {})

                config_template_refs = script_fields.get(
                    "Label Configuration Template", {}
                ).get("value", [])

                if (
                    not config_template_refs
                    or not isinstance(config_template_refs, list)
                    or len(config_template_refs) == 0
                ):
                    raise Exception(
                        "Manual mode failed: No 'Label Configuration Template' reference field found in script entity. "
                        "Please add a 'Label Configuration Template' reference field to the script."
                    )

                config_template_id = config_template_refs[0].get("id", "")

                if not config_template_id:
                    raise Exception(
                        "Manual mode failed: Label Configuration Template reference is invalid"
                    )

                # Find configuration instance that supports containing entity's template
                config_result = find_config_for_template(
                    config_template_id, containing_template_id
                )

                if config_result["error"]:
                    error_details = config_result["error"]
                    raise Exception(
                        f"Manual mode failed: Configuration lookup failed for template "
                        f"'{containing_template_id}': {error_details}\n"
                        f"Please ensure you have exactly one configuration instance from the "
                        f"Label Configuration Template with '{containing_template_id}' in the "
                        f"'Templates' field."
                    )

                config_entity_id = config_result["matches"][0]["id"]

                config_entity = seal.get_entity(entity_id=config_entity_id)
                config_field_name = f"Manual Mode (template {containing_template_id})"
                print(
                    f"Manual mode: Found configuration for template '{containing_template_id}'"
                )

            # Validate that the script has a properly configured Label Configuration Template (Preview mode only)
            if mode == "preview":
                preview_entities_refs = (
                    config_entity.get("fields", {})
                    .get("Preview Entities", {})
                    .get("value", [])
                )

                if preview_entities_refs:
                    # Check if script entity has Label Configuration Template field
                    script_entity_id = seal.entity_id
                    script_entity = seal.get_entity(entity_id=script_entity_id)
                    script_fields = script_entity.get("fields", {})

                    config_template_refs = script_fields.get(
                        "Label Configuration Template", {}
                    ).get("value", [])

                    if (
                        not config_template_refs
                        or not isinstance(config_template_refs, list)
                        or len(config_template_refs) == 0
                    ):
                        print(
                            "INFO: No 'Label Configuration Template' reference field found in script entity. "
                            "Preview will work, but this script won't trigger in trigger. "
                            "To enable trigger mode, add a 'Label Configuration Template' "
                            "reference field to the script entity."
                        )
                        config_field_name = "Preview Mode (no trigger configuration)"
                        # Skip further validation since there's no Label Configuration Template
                        preview_validation_complete = True
                    else:
                        config_template_id = config_template_refs[0].get("id", "")
                        preview_validation_complete = False

                    # Check that preview entities' templates are covered in Label Configuration Template
                    if not preview_validation_complete:
                        first_preview_entity = seal.get_entity(
                            entity_id=preview_entities_refs[0]["id"]
                        )
                        preview_template_ref = first_preview_entity.get(
                            "sourceInfo", {}
                        ).get("template", {})
                        preview_template_id = preview_template_ref.get("id", "")

                        if preview_template_id:
                            # Check if Label Configuration Template instances support this template
                            config_result = find_config_for_template(
                                config_template_id, preview_template_id
                            )

                            if config_result["error"]:
                                error_details = config_result["error"]
                                print(
                                    f"WARNING: Configuration lookup failed for preview entity "
                                    f"template '{preview_template_id}': {error_details}. "
                                    f"This preview will generate successfully, but entities with this "
                                    f"template will NOT trigger the script in trigger mode. To enable "
                                    f"trigger mode, ensure you have exactly one configuration instance "
                                    f"from the Label Configuration Template with '{preview_template_id}' "
                                    f"in the 'Templates' field."
                                )
                                config_field_name = (
                                    "Preview Mode (template not configured for trigger)"
                                )
                            elif (
                                config_result["matches"][0]["id"] != config_entity["id"]
                            ):
                                found_config_id = config_result["matches"][0]["id"]
                                found_config_title = config_result["matches"][0][
                                    "title"
                                ]
                                raise Exception(
                                    f"Script validation failed: Label Configuration Template instances "
                                    f"contain template '{preview_template_id}' but point to config "
                                    f"'{found_config_title}' (ID: {found_config_id}) instead of this "
                                    f"configuration entity '{config_entity['id']}'. Please update the "
                                    f"configuration instance."
                                )
                            else:
                                config_field_name = "Preview Mode (validated Label Configuration Template)"
                        else:
                            print(
                                "Preview entities found but first entity has no template - "
                                "skipping script validation"
                            )
                else:
                    print("No preview entities configured - skipping script validation")
                    config_field_name = "Preview Mode"

        except Exception as preview_error:
            raise Exception(
                f"Cannot determine configuration entity.\n"
                f"Trigger mode failed: {str(e)}\n"
                f"Embedded mode failed: {str(preview_error)}"
            )

    if not config_entity:
        raise Exception("Could not load configuration entity")

    print(f"Using configuration: {config_field_name} (ID: {config_entity['id']})")

    fields = config_entity.get("fields", {})

    units = extract_field_value(fields, "Units", "cm")

    label_width = extract_field_value(fields, "Label Width", 10.0)
    label_height = extract_field_value(fields, "Label Height", 5.0)
    print(f"Label configuration: {label_width} x {label_height} {units}")

    config = {
        "Label Width": label_width,
        "Label Height": label_height,
        "Default Font": extract_field_value(fields, "Default Font", DEFAULT_FONT_NAME),
        "Default Font Size": extract_field_value(
            fields, "Default Font Size", DEFAULT_FONT_SIZE
        ),
        "Units": units,
        "Preview Entities": fields.get("Preview Entities", {}).get("value", []),
        "Show Grid Lines": False,  # Default to False, will be overridden in preview mode if needed
    }

    # Load layout elements from Label Elements reference field
    layout_elements_field = fields.get("Label Elements", {})
    layout_elements = []

    if layout_elements_field and "value" in layout_elements_field:
        element_refs = layout_elements_field["value"]

        if not element_refs:
            print("Label Elements field is empty - no elements will be rendered")

        for element_ref in element_refs:
            try:
                element_entity = seal.get_entity(entity_id=element_ref["id"])
                element_fields = element_entity.get("fields", {})

                # Extract element configuration with defaults
                element = {
                    "Element Type": extract_field_value(
                        element_fields, "Element Type", "Text"
                    ),
                    "Text": extract_field_value(
                        element_fields, "Text", ""
                    ),  # For Text elements
                    "Field Or Property Name": extract_field_value(
                        element_fields, "Field Or Property Name", ""
                    ),  # For all other elements
                    "Column Names": extract_field_value(
                        element_fields, "Column Names", ""
                    ),  # For Table elements - comma-separated text field
                    "Prefix Text": extract_field_value(
                        element_fields, "Prefix Text", ""
                    ),
                    "X Position": extract_field_value(element_fields, "X Position", 0),
                    "Y Position": extract_field_value(element_fields, "Y Position", 0),
                    "Width": extract_field_value(element_fields, "Width", 2.0),
                    "Height": extract_field_value(element_fields, "Height", 1.0),
                    "Font Name": extract_field_value(
                        element_fields, "Font Name", DEFAULT_FONT_NAME
                    ),
                    "Font Size": extract_field_value(
                        element_fields, "Font Size", None
                    ),  # None = use default
                    "Alignment": extract_field_value(
                        element_fields, "Alignment", "Top-Left"
                    ),  # Universal alignment
                    "Rotation (°)": extract_field_value(
                        element_fields, "Rotation (°)", 0
                    ),  # Rotation in degrees
                    "Line Width": extract_field_value(element_fields, "Line Width", 1),
                    "Stroke Color": extract_field_value(
                        element_fields, "Stroke Color", "Black"
                    ),
                }
                layout_elements.append(element)

            except Exception as e:
                print(f"Failed to load layout element {element_ref['id']}: {str(e)}")
                continue

    config["Label Elements"] = layout_elements
    config["Configuration Entity ID"] = config_entity["id"]

    # Add mode information for tri-mode detection
    if "mode" in locals():
        config["_mode_info"] = {
            "mode": mode,
            "containing_entity_id": containing_entity_id if mode == "manual" else None,
        }

        # In preview mode, check for "Show Gridlines" field
        if mode == "preview":
            show_gridlines_field = fields.get(
                "Show Gridlines (only shown in preview mode)", {"value": False}
            )
            config["Show Grid Lines"] = show_gridlines_field.get("value")
            if config["Show Grid Lines"]:
                print("Grid lines enabled via 'Show Gridlines' field")

    print(
        f"Loaded {len(layout_elements)} label elements using config '{config_field_name}'"
    )
    return config


def convert_coordinates(x, y, canvas_obj):
    """
    Convert from top-left coordinate system to ReportLab's bottom-left system

    User coordinates: (0,0) = top-left, Y increases downward
    ReportLab: (0,0) = bottom-left, Y increases upward
    """
    height = canvas_obj._label_height
    return x, height - y


def calculate_aligned_position(x, y, width, height, alignment):
    """
    Calculate actual drawing position based on alignment anchor point

    Args:
        x, y: User-specified coordinates
        width, height: Element dimensions (in same units as x, y)
        alignment: "Top-Left", "Center", "Top-Right", "Bottom-Left", "Bottom-Right" (case-insensitive)

    Returns:
        (adjusted_x, adjusted_y): Actual position for drawing
    """
    # Make alignment detection case-insensitive
    alignment_upper = alignment.upper()

    if alignment_upper == "CENTER" or alignment_upper == "CENTRE":
        return x - width / 2, y - height / 2
    elif alignment_upper == "TOP-RIGHT":
        return x - width, y
    elif alignment_upper == "BOTTOM-LEFT":
        return x, y - height
    elif alignment_upper == "BOTTOM-RIGHT":
        return x - width, y - height
    else:  # "TOP-LEFT" or any other value defaults to top-left
        return x, y


def apply_rotation_to_canvas(canvas_obj, rotation_degrees, anchor_x, anchor_y):
    """
    Apply rotation around a specific anchor point

    Args:
        canvas_obj: ReportLab canvas object
        rotation_degrees: Rotation angle in degrees (clockwise)
        anchor_x, anchor_y: Point to rotate around (in ReportLab coordinates)
    """
    if rotation_degrees != 0:
        # Save current state
        canvas_obj.saveState()

        # Translate to anchor point, rotate, then translate back
        canvas_obj.translate(anchor_x, anchor_y)
        canvas_obj.rotate(rotation_degrees)
        canvas_obj.translate(-anchor_x, -anchor_y)

        return True  # Indicates rotation was applied
    return False  # No rotation applied


def render_element_with_rotation(canvas_obj, element, x, y, unit_obj, render_callback):
    """
    Base render function that handles common rotation setup/teardown for all element types

    Args:
        canvas_obj: ReportLab canvas object
        element: Element configuration dict
        x, y: User coordinates
        unit_obj: ReportLab unit object (cm, mm, inch)
        render_callback: Function that performs the actual rendering
                        Signature: render_callback(canvas_obj, element, x, y, unit_obj, anchor_coords)
                        where anchor_coords is (anchor_x_rl, anchor_y_rl)
    """
    rotation = element.get("Rotation (°)", 0)
    if isinstance(rotation, list) and rotation:
        rotation = rotation[0]
    if not isinstance(rotation, (int, float)):
        rotation = 0

    anchor_x = x * unit_obj
    anchor_y = y * unit_obj
    anchor_x_rl, anchor_y_rl = convert_coordinates(anchor_x, anchor_y, canvas_obj)

    rotation_applied = apply_rotation_to_canvas(
        canvas_obj, rotation, anchor_x_rl, anchor_y_rl
    )

    try:
        render_callback(canvas_obj, element, x, y, unit_obj, (anchor_x_rl, anchor_y_rl))

    finally:
        if rotation_applied:
            canvas_obj.restoreState()


def check_page_break(y_position, canvas_obj, min_space=50):
    """
    Check if we need a page break and create one if necessary
    Returns the new Y position (reset to top if page break occurred)
    """
    if y_position < min_space:  # Less than min_space points from bottom
        canvas_obj.showPage()
        # After page break, we're back to top-left coordinates
        return canvas_obj._label_height - 20  # Start 20 points from top
    return y_position


def draw_grid_lines(canvas_obj, label_width, label_height, units, unit_obj):
    """
    Draw grid lines to help with element positioning (preview mode only)

    Args:
        canvas_obj: ReportLab canvas object
        label_width: Label width in user units
        label_height: Label height in user units
        units: Unit type ("cm", "mm", "inch")
        unit_obj: ReportLab unit object
    """
    # Grid colors (subtle so they don't interfere with content)
    major_grid_color = (0.7, 0.7, 0.7)  # Light gray for major lines
    minor_grid_color = (0.9, 0.9, 0.9)  # Very light gray for minor lines

    # Define grid spacing based on units
    if units == "cm":
        major_spacing = 1.0  # Every 1 cm
        minor_spacing = 0.5  # Every 0.5 cm
    elif units == "mm":
        major_spacing = 10.0  # Every 10 mm
        minor_spacing = 5.0  # Every 5 mm
    elif units == "inch":
        major_spacing = 1.0  # Every 1 inch
        minor_spacing = 0.25  # Every 0.25 inch
    else:
        major_spacing = 1.0
        minor_spacing = 0.5

    # Convert to points
    width_points = label_width * unit_obj
    height_points = label_height * unit_obj

    canvas_obj.saveState()

    # Draw minor grid lines first (so they appear behind major lines)
    canvas_obj.setStrokeColorRGB(*minor_grid_color)
    canvas_obj.setLineWidth(0.25)

    # Vertical minor lines
    x = minor_spacing
    while x < label_width:
        x_points = x * unit_obj
        canvas_obj.line(x_points, 0, x_points, height_points)
        x += minor_spacing

    # Horizontal minor lines
    y = minor_spacing
    while y < label_height:
        y_points = height_points - (y * unit_obj)  # Convert to ReportLab coordinates
        canvas_obj.line(0, y_points, width_points, y_points)
        y += minor_spacing

    # Draw major grid lines
    canvas_obj.setStrokeColorRGB(*major_grid_color)
    canvas_obj.setLineWidth(0.5)

    # Vertical major lines with labels
    x = major_spacing
    while x <= label_width:
        x_points = x * unit_obj
        canvas_obj.line(x_points, 0, x_points, height_points)

        # Add coordinate labels at the top
        canvas_obj.setFont("Helvetica", 6)
        canvas_obj.setFillColorRGB(*major_grid_color)
        label_text = f"{int(x)}" if x == int(x) else f"{x:.1f}"
        canvas_obj.drawCentredString(x_points, height_points - 10, label_text)

        x += major_spacing

    # Horizontal major lines with labels
    y = major_spacing
    while y <= label_height:
        y_points = height_points - (y * unit_obj)  # Convert to ReportLab coordinates
        canvas_obj.line(0, y_points, width_points, y_points)

        # Add coordinate labels on the left
        canvas_obj.setFont("Helvetica", 6)
        canvas_obj.setFillColorRGB(*major_grid_color)
        label_text = f"{int(y)}" if y == int(y) else f"{y:.1f}"
        canvas_obj.drawString(3, y_points - 2, label_text)

        y += major_spacing

    # Draw border
    canvas_obj.setStrokeColorRGB(*major_grid_color)
    canvas_obj.setLineWidth(1.0)
    canvas_obj.rect(0, 0, width_points, height_points, stroke=1, fill=0)

    # Add unit labels in corners
    canvas_obj.setFont("Helvetica", 8)
    canvas_obj.setFillColorRGB(*major_grid_color)
    canvas_obj.drawString(5, height_points - 15, f"Preview Mode Grid: {units}")
    canvas_obj.drawRightString(
        width_points - 5, 5, f"Size: {label_width} x {label_height} {units}"
    )

    canvas_obj.restoreState()


def create_label_pdf(config, entity_data):
    """
    SHARED label generation core used by both modes

    Args:
        config: Dict of configuration values (width, height, label elements, etc.)
        entity_data: Dict of entity field values

    Returns:
        PDF buffer ready for upload
    """
    print("Starting PDF generation...")

    # Initialize entity cache for Table elements
    if "_entity_cache" not in config:
        config["_entity_cache"] = {}

        # Pre-populate cache with entities from reference fields
        source_entity = entity_data.get("_source_entity", {})
        entity_refs_to_fetch = collect_entity_references_from_entity(source_entity)
        if entity_refs_to_fetch:
            config["_entity_cache"] = batch_fetch_entities(
                entity_refs_to_fetch, config["_entity_cache"]
            )

    units = config.get("Units", "cm")
    unit_obj = UNIT_OBJECTS[units]

    width = config.get("Label Width", 10.0) * unit_obj
    height = config.get("Label Height", 5.0) * unit_obj

    pdf_buffer = io.BytesIO()
    canvas_obj = canvas.Canvas(pdf_buffer, pagesize=(width, height))

    # Store dimensions for coordinate conversion
    canvas_obj._label_width = width
    canvas_obj._label_height = height

    # Draw grid lines in preview mode if enabled
    if config.get("Show Grid Lines", False):
        draw_grid_lines(
            canvas_obj,
            config.get("Label Width", 10.0),
            config.get("Label Height", 5.0),
            units,
            unit_obj,
        )

    layout_elements = config.get("Label Elements", [])
    print(f"Processing {len(layout_elements)} label elements")

    for i, element in enumerate(layout_elements):
        try:
            render_layout_element(canvas_obj, element, entity_data, unit_obj, config)
        except Exception as e:
            print(f"Failed to render layout element {i + 1}: {str(e)}")
            continue

    canvas_obj.save()
    pdf_buffer.seek(0)
    return pdf_buffer


def render_layout_element(canvas_obj, element, entity_data, unit_obj, config):
    """Render a single layout element on the canvas"""
    element_type = element.get("Element Type", "").strip()

    # Handle Table element type
    if element_type == "Table":
        x_pos = element.get("X Position", 0)
        y_pos = element.get("Y Position", 0)
        alignment = element.get("Alignment", "Top-Left")
        if isinstance(alignment, list) and alignment:
            alignment = alignment[0].strip()
        elif isinstance(alignment, str):
            alignment = alignment.strip()
        else:
            alignment = "Top-Left"
        render_table_element(
            canvas_obj, element, entity_data, x_pos, y_pos, unit_obj, config, alignment
        )
        return

    if element_type == "Text":
        value_or_field_name = element.get("Text", "").strip()
    else:
        value_or_field_name = element.get("Field Or Property Name", "").strip()

    x_pos = element.get("X Position", 0)
    y_pos = element.get("Y Position", 0)

    alignment = element.get("Alignment", "Top-Left")
    if isinstance(alignment, list) and alignment:
        alignment = alignment[0].strip()
    elif isinstance(alignment, str):
        alignment = alignment.strip()
    else:
        alignment = "Top-Left"
    x = x_pos * unit_obj
    y = y_pos * unit_obj

    # Convert from top-left to bottom-left coordinates
    x_final, y_final = convert_coordinates(x, y, canvas_obj)

    if element_type == "Page Break":
        canvas_obj.showPage()
        return

    prefix_text = element.get("Prefix Text", "")

    if element_type == "Text":
        base_value = value_or_field_name
    elif element_type == "Field":
        # Handle both entity properties and field values
        if is_entity_property(value_or_field_name):
            # Get from entity properties (need the full entity)
            source_entity = entity_data.get("_source_entity", {})
            base_value = get_field_or_property_value(source_entity, value_or_field_name)
        else:
            # Get from entity fields using utility (supports case-insensitive properties)
            source_entity = entity_data.get("_source_entity", {})
            base_value = get_field_or_property_value(source_entity, value_or_field_name)

        if not base_value and value_or_field_name:
            print(
                f"Field or property '{value_or_field_name}' not found in entity data, skipping element"
            )
            return
    elif element_type in ["Barcode Code128", "QR Code", "Barcode Code39"]:
        # Get from entity properties (need the full entity)
        source_entity = entity_data.get("_source_entity", {})
        base_value = get_field_or_property_value(source_entity, value_or_field_name)

        if not base_value and value_or_field_name:
            print(
                f"Field or property '{value_or_field_name}' not found in entity data, skipping element"
            )
            return
        # For barcodes, use base value without prefix (barcodes need clean data)
        final_value = base_value
    elif element_type == "Image":
        source_entity = entity_data.get("_source_entity", {})
        image_refs = get_field_or_property_value(source_entity, value_or_field_name)

        if not image_refs:
            print(
                f"Image reference field or property '{value_or_field_name}' not found, skipping element"
            )
            return
        final_value = image_refs
    elif element_type == "Border":
        base_value = ""
        final_value = ""
    else:
        print(f"Unknown element type: {element_type}")
        return

    if element_type in ["Text", "Field"]:
        final_value = f"{prefix_text}{base_value}" if prefix_text else base_value
    # Render the appropriate element
    if element_type in ["Text", "Field"]:
        render_text_element(
            canvas_obj, element, final_value, x_pos, y_pos, unit_obj, config, alignment
        )
    elif element_type in ["Barcode Code128", "QR Code", "Barcode Code39"]:
        render_barcode_element(
            canvas_obj, element, final_value, x_pos, y_pos, unit_obj, alignment
        )
    elif element_type == "Image":
        render_image_element(
            canvas_obj, element, final_value, x_pos, y_pos, unit_obj, alignment
        )
    elif element_type == "Border":
        render_rectangle_element(canvas_obj, element, x_pos, y_pos, unit_obj, alignment)


def render_text_element(
    canvas_obj, element, field_value, x, y, unit_obj, config, alignment
):
    """Render text element with universal alignment and rotation support"""

    def render_text_callback(canvas_obj, element, x, y, unit_obj, anchor_coords):
        """Specific text rendering logic"""
        font_name = element.get("Font Name (optional)", None)
        if font_name is None:
            font_name = config.get("Default Font", DEFAULT_FONT_NAME)
        font_size = element.get("Font Size (optional)", None)
        if font_size is None:
            font_size = config.get("Default Font Size", DEFAULT_FONT_SIZE)
        stroke_color = COLOR_MAP.get(element.get("Stroke Color", "Black"), black)

        alignment_upper = alignment.upper()
        if "RIGHT" in alignment_upper:
            text_alignment = "Right"
        elif "CENTER" in alignment_upper or "CENTRE" in alignment_upper:
            text_alignment = "Center"
        else:
            text_alignment = "Left"

        text_str = str(field_value)

        # Split text by newlines for multiline support
        text_lines = text_str.split("\n")

        # Calculate dimensions for all lines
        max_text_width = 0
        for line in text_lines:
            line_width = stringWidth(line, font_name, font_size)
            max_text_width = max(max_text_width, line_width)

        # Calculate total text height (font size + line spacing for each line)
        line_spacing = font_size * 0.2  # 20% of font size for line spacing
        total_text_height = (
            len(text_lines) * font_size + (len(text_lines) - 1) * line_spacing
        )

        # Use the same alignment approach as images - calculate aligned position and stick with it
        aligned_x, aligned_y = calculate_aligned_position(
            x, y, max_text_width / unit_obj, total_text_height / unit_obj, alignment
        )

        # Convert aligned position to ReportLab coordinates
        aligned_x_final = aligned_x * unit_obj
        aligned_y_final = aligned_y * unit_obj
        aligned_x_rl, aligned_y_rl = convert_coordinates(
            aligned_x_final, aligned_y_final, canvas_obj
        )

        canvas_obj.setFont(font_name, font_size)
        if stroke_color:
            canvas_obj.setFillColor(stroke_color)

        # For center alignment, we need the original coordinates (where user wants the center)
        if text_alignment == "Center":
            original_x_rl = x * unit_obj  # Original x coordinate in ReportLab units

        # Draw each line separately
        for line_index, line in enumerate(text_lines):
            # Calculate Y position for this line (ReportLab draws from baseline)
            line_y = (
                aligned_y_rl
                - font_size * 0.8
                - (line_index * (font_size + line_spacing))
            )

            if text_alignment == "Center":
                # For center: use the original coordinates as the center point (avoid double-centering)
                canvas_obj.drawCentredString(original_x_rl, line_y, line)
            elif text_alignment == "Right":
                # For right: use the right edge of the aligned text area
                right_x = aligned_x_rl + max_text_width
                canvas_obj.drawRightString(right_x, line_y, line)
            else:
                # For left: use the left edge of the aligned text area
                canvas_obj.drawString(aligned_x_rl, line_y, line)

    render_element_with_rotation(
        canvas_obj, element, x, y, unit_obj, render_text_callback
    )


def render_barcode_element(canvas_obj, element, field_value, x, y, unit_obj, alignment):
    """Render barcode element with universal alignment and rotation support"""

    def render_barcode_callback(canvas_obj, element, x, y, unit_obj, anchor_coords):
        """Specific barcode rendering logic"""
        element_type = element.get("Element Type")
        desired_width = element.get("Width", 2.0) * unit_obj
        height = element.get("Height", 1.0) * unit_obj

        try:
            canvas_obj.saveState()

            canvas_obj.setStrokeColor(black)
            canvas_obj.setFillColor(black)

            if element_type == "Barcode Code128":
                # Calculate appropriate barWidth for desired total width
                # For Code128, typical character width is about 11 units, so estimate total units needed
                estimated_units = (
                    len(str(field_value)) * 11 + 35
                )  # 35 for start/stop/checksum
                target_bar_width = desired_width / estimated_units

                # Ensure minimum bar width for readability (at least 0.5 points)
                min_bar_width = 0.5
                bar_width = max(target_bar_width, min_bar_width)

                barcode = code128.Code128(
                    value=str(field_value),
                    barHeight=height,
                    barWidth=bar_width,
                    humanReadable=True,
                    quiet=0,  # Disable quiet zones for precise alignment
                    lquiet=0,
                    rquiet=0,
                )

                actual_barcode_width = barcode.width

                draw_x, draw_y = calculate_aligned_position(
                    x, y, actual_barcode_width / unit_obj, height / unit_obj, alignment
                )
                draw_x_final = draw_x * unit_obj
                draw_y_final = draw_y * unit_obj
                draw_x_rl, draw_y_rl = convert_coordinates(
                    draw_x_final, draw_y_final, canvas_obj
                )

                # Adjust Y coordinate: ReportLab draws barcodes from bottom-left, we want top-left positioning
                actual_barcode_y = draw_y_rl - height

                barcode.drawOn(canvas_obj, draw_x_rl, actual_barcode_y)

            elif element_type == "Barcode Code39":
                # Calculate appropriate barWidth for desired total width
                # For Code39, each character is about 13 units wide
                estimated_units = len(str(field_value)) * 13 + 25  # 25 for start/stop
                target_bar_width = desired_width / estimated_units

                # Ensure minimum bar width for readability
                min_bar_width = 0.5
                bar_width = max(target_bar_width, min_bar_width)

                barcode = code39.Standard39(
                    value=str(field_value),
                    barHeight=height,
                    barWidth=bar_width,
                    humanReadable=True,
                    quiet=0,  # Disable quiet zones for precise alignment
                    lquiet=0,
                    rquiet=0,
                )

                actual_barcode_width = barcode.width

                draw_x, draw_y = calculate_aligned_position(
                    x, y, actual_barcode_width / unit_obj, height / unit_obj, alignment
                )
                draw_x_final = draw_x * unit_obj
                draw_y_final = draw_y * unit_obj
                draw_x_rl, draw_y_rl = convert_coordinates(
                    draw_x_final, draw_y_final, canvas_obj
                )

                # Adjust Y coordinate: ReportLab draws barcodes from bottom-left, we want top-left positioning
                actual_barcode_y = draw_y_rl - height

                barcode.drawOn(canvas_obj, draw_x_rl, actual_barcode_y)

            elif element_type == "QR Code":
                # QR codes use the width and height directly
                draw_x, draw_y = calculate_aligned_position(
                    x, y, desired_width / unit_obj, height / unit_obj, alignment
                )
                draw_x_final = draw_x * unit_obj
                draw_y_final = draw_y * unit_obj
                draw_x_rl, draw_y_rl = convert_coordinates(
                    draw_x_final, draw_y_final, canvas_obj
                )

                qr_code = qr.QrCodeWidget(str(field_value))
                qr_code.barWidth = desired_width
                qr_code.barHeight = height

                from reportlab.graphics import renderPDF  # type: ignore
                from reportlab.graphics.shapes import Drawing  # type: ignore

                drawing = Drawing(desired_width, height)
                drawing.add(qr_code)

                # Adjust Y coordinate: ReportLab draws from bottom-left, we want top-left positioning
                qr_y = draw_y_rl - height
                renderPDF.draw(drawing, canvas_obj, draw_x_rl, qr_y)

            canvas_obj.restoreState()

        except Exception as e:
            print(f"Failed to render barcode {element_type}: {str(e)}")
            canvas_obj.restoreState()

            fallback_x, fallback_y = calculate_aligned_position(
                x, y, 100 / unit_obj, 8 / unit_obj, alignment
            )
            fallback_x_final = fallback_x * unit_obj
            fallback_y_final = fallback_y * unit_obj
            fallback_x_rl, fallback_y_rl = convert_coordinates(
                fallback_x_final, fallback_y_final, canvas_obj
            )

            canvas_obj.setFont("Helvetica", 8)
            fallback_text_y = fallback_y_rl - 8 * 0.8
            canvas_obj.drawString(
                fallback_x_rl, fallback_text_y, f"Barcode: {field_value}"
            )

    render_element_with_rotation(
        canvas_obj, element, x, y, unit_obj, render_barcode_callback
    )


def render_rectangle_element(canvas_obj, element, x, y, unit_obj, alignment):
    """Render border element with universal alignment and rotation support"""

    def render_rectangle_callback(canvas_obj, element, x, y, unit_obj, anchor_coords):
        """Specific rectangle/border rendering logic"""
        width = element.get("Width", 2.0) * unit_obj
        height = element.get("Height", 1.0) * unit_obj
        line_width = element.get("Line Width", 1)
        stroke_color = COLOR_MAP.get(element.get("Stroke Color", "Black"))

        aligned_x, aligned_y = calculate_aligned_position(
            x, y, width / unit_obj, height / unit_obj, alignment
        )

        x_final = aligned_x * unit_obj
        y_final = aligned_y * unit_obj
        x_rl, y_rl = convert_coordinates(x_final, y_final, canvas_obj)

        canvas_obj.setLineWidth(line_width)
        if stroke_color:
            canvas_obj.setStrokeColor(stroke_color)

        # Adjust Y coordinate: ReportLab draws rectangles from bottom-left, we want top-left positioning
        rect_y = y_rl - height

        stroke_flag = 1 if stroke_color else 0
        canvas_obj.rect(x_rl, rect_y, width, height, stroke=stroke_flag, fill=0)

    render_element_with_rotation(
        canvas_obj, element, x, y, unit_obj, render_rectangle_callback
    )


def render_table_element(
    canvas_obj, element, entity_data, x, y, unit_obj, config, alignment
):
    """Render table using ReportLab's Table class"""

    def render_table_callback(canvas_obj, element, x, y, unit_obj, anchor_coords):
        """Specific table rendering logic"""
        # Get table configuration
        field_name = element.get("Field Or Property Name", "").strip()
        column_names_text = element.get("Column Names", "").strip()

        print(f"Column Names: {column_names_text}")

        # Parse comma-separated column names into a list
        if column_names_text:
            column_names = [
                name.strip() for name in column_names_text.split(",") if name.strip()
            ]
        else:
            column_names = []

        if not field_name or not column_names:
            return

        # Get entity references from the specified field
        # For table elements, we need the FULL array of references, not just the first one
        source_entity = entity_data.get("_source_entity", {})

        if is_entity_property(field_name):
            entity_refs = get_field_or_property_value(source_entity, field_name)
        else:
            fields = source_entity.get("fields", {})
            entity_refs = fields.get(field_name, {}).get("value", [])

        if not entity_refs:
            print(f"DEBUG: No entity references found in field '{field_name}'")
            return

        entity_cache = config.get("_entity_cache", {})

        entity_ids_to_fetch = []
        for ref in entity_refs:
            if isinstance(ref, dict) and "id" in ref:
                entity_id = ref["id"]
                if entity_id not in entity_cache:
                    entity_ids_to_fetch.append(entity_id)

        # Batch fetch any missing entities
        if entity_ids_to_fetch:
            entity_cache = batch_fetch_entities(entity_ids_to_fetch, entity_cache)
            config["_entity_cache"] = entity_cache

        # Build table data
        table_data = []

        # Header row - use column names as headers
        headers = column_names[:]  # Copy the list
        table_data.append(headers)

        # Data rows
        for ref in entity_refs:
            if isinstance(ref, dict) and "id" in ref:
                entity_id = ref["id"]
                if entity_id in entity_cache and entity_cache[entity_id]:
                    entity = entity_cache[entity_id]
                    row = []

                    for column_name in column_names:
                        value = get_field_or_property_value(entity, column_name)
                        row.append(str(value) if value else "")

                    table_data.append(row)

        if len(table_data) <= 1:  # Only headers
            return

        # Get font size for the table - use element font size or fall back to global default
        table_font_size = element.get("Font Size")
        if table_font_size is None:
            table_font_size = config.get("Default Font Size", DEFAULT_FONT_SIZE)
        if isinstance(table_font_size, list) and table_font_size:
            table_font_size = table_font_size[0]
        if not isinstance(table_font_size, (int, float)) or table_font_size <= 0:
            table_font_size = config.get("Default Font Size", DEFAULT_FONT_SIZE)

        # Get font name for the table - use element font name or fall back to global default
        table_font_name = element.get("Font Name (optional)")
        if table_font_name is None:
            table_font_name = config.get("Default Font", DEFAULT_FONT_NAME)

        # Get bold version of the font for headers
        if table_font_name == "Helvetica":
            table_font_bold = "Helvetica-Bold"
        elif table_font_name == "Times-Roman":
            table_font_bold = "Times-Bold"
        elif table_font_name == "Courier":
            table_font_bold = "Courier-Bold"
        else:
            # For unknown fonts, try adding -Bold or fall back to the same font
            table_font_bold = (
                f"{table_font_name}-Bold"
                if not table_font_name.endswith("-Bold")
                else table_font_name
            )

        # Calculate dimensions first - needed for column width and row height calculation
        cell_padding = table_font_size * 0.3
        border_width = table_font_size * 0.05  # 5% of font size for border thickness
        row_height = (
            table_font_size * 1.8
        )  # 180% of font size for row height (extra space for vertical centering)

        min_col_widths = []
        for col_index in range(len(column_names)):
            max_width = 0
            for row in table_data:
                if col_index < len(row):
                    cell_text = str(row[col_index])
                    # Use header font for header row, data font for data rows
                    font_name = (
                        table_font_bold if row == table_data[0] else table_font_name
                    )
                    text_width = stringWidth(cell_text, font_name, table_font_size)
                    max_width = max(max_width, text_width)

            # Add padding (both sides) plus some extra margin for large fonts
            padding_multiplier = 0.5  # More padding for large fonts
            min_width = (
                max_width + (cell_padding * 2) + (table_font_size * padding_multiplier)
            )
            min_col_widths.append(min_width)

        # Create explicit row heights for proper vertical alignment
        row_heights = [row_height] * len(table_data)  # Same height for all rows

        # Create ReportLab Table with calculated column widths AND explicit row heights
        table = Table(table_data, colWidths=min_col_widths, rowHeights=row_heights)

        # Build complete style commands list including row heights
        style_commands = [
            # Header row styling - background and font first
            ("BACKGROUND", (0, 0), (-1, 0), colors.gray),
            ("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
            ("FONTNAME", (0, 0), (-1, 0), table_font_bold),
            ("FONTSIZE", (0, 0), (-1, 0), table_font_size),
            # Data rows styling - background and font
            ("BACKGROUND", (0, 1), (-1, -1), colors.white),
            ("TEXTCOLOR", (0, 1), (-1, -1), colors.black),
            ("FONTNAME", (0, 1), (-1, -1), table_font_name),
            ("FONTSIZE", (0, 1), (-1, -1), table_font_size),
            # Row backgrounds for readability - alternating white and light gray
            ("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, (0.9, 0.9, 0.9)]),
            # Grid and borders - thickness relative to font size
            ("GRID", (0, 0), (-1, -1), border_width, colors.black),
            # Alignment commands LAST - so they take precedence
            ("VALIGN", (0, 0), (-1, -1), "MIDDLE"),  # Vertical center
            ("ALIGN", (0, 0), (-1, 0), "CENTER"),  # Header row center
            ("ALIGN", (0, 1), (-1, -1), "CENTER"),  # Data rows center
        ]

        # Note: Row heights are now set in Table constructor, not via MINHEIGHT style commands
        # This ensures proper vertical alignment with VALIGN

        table_style = TableStyle(style_commands)
        table.setStyle(table_style)

        # CRITICAL: Wrap the table to calculate natural dimensions (ReportLab requirement)
        canvas_width = canvas_obj._label_width
        canvas_height = canvas_obj._label_height

        try:
            actual_table_width, actual_table_height = table.wrap(
                canvas_width, canvas_height
            )
        except (ValueError, TypeError):
            # Fallback: calculate dimensions from our explicit column widths and row count
            actual_table_width = sum(min_col_widths)
            actual_table_height = len(table_data) * row_height

        # Calculate position with alignment using ACTUAL table dimensions
        aligned_x, aligned_y = calculate_aligned_position(
            x,
            y,
            actual_table_width / unit_obj,
            actual_table_height / unit_obj,
            alignment,
        )

        final_x = aligned_x * unit_obj
        final_y = aligned_y * unit_obj

        final_x_rl, final_y_rl = convert_coordinates(final_x, final_y, canvas_obj)

        # Draw table (ReportLab tables draw from bottom-left)
        table_y = final_y_rl - actual_table_height

        table.drawOn(canvas_obj, final_x_rl, table_y)

    # Use existing rotation framework
    # Note: render_table_callback accesses entity_data and config from closure
    render_element_with_rotation(
        canvas_obj, element, x, y, unit_obj, render_table_callback
    )


def render_image_element(canvas_obj, element, image_refs, x, y, unit_obj, alignment):
    """Render image element with universal alignment and rotation support"""

    def render_image_callback(canvas_obj, element, x, y, unit_obj, anchor_coords):
        """Specific image rendering logic"""
        width = element.get("Width", 2.0) * unit_obj
        height = element.get("Height", 1.0) * unit_obj

        aligned_x, aligned_y = calculate_aligned_position(
            x, y, width / unit_obj, height / unit_obj, alignment
        )

        x_final = aligned_x * unit_obj
        y_final = aligned_y * unit_obj
        x_rl, y_rl = convert_coordinates(x_final, y_final, canvas_obj)

        try:
            if isinstance(image_refs, list) and image_refs:
                image_ref = image_refs[0]
            elif isinstance(image_refs, dict):
                image_ref = image_refs
            else:
                print(f"Invalid image reference format: {image_refs}")
                return

            image_entity_id = image_ref.get("id", "")
            if not image_entity_id:
                print("No image entity ID found in reference")
                return

            if image_ref.get("version"):
                temp_path = seal.download_file(
                    file_entity_id=image_entity_id, version=image_ref["version"]
                )
            else:
                temp_path = seal.download_file(file_entity_id=image_entity_id)

            try:
                # Adjust Y coordinate: ReportLab draws images from bottom-left, we want top-left positioning
                image_y = y_rl - height

                canvas_obj.drawImage(
                    temp_path, x_rl, image_y, width=width, height=height, mask="auto"
                )

            finally:
                try:
                    os.unlink(temp_path)
                except Exception as cleanup_error:
                    print(f"Failed to clean up downloaded file: {cleanup_error}")

        except Exception as e:
            print(f"Failed to render image: {str(e)}")
            canvas_obj.setFont("Helvetica", 8)
            fallback_y = y_rl - 8 * 0.8
            canvas_obj.drawString(x_rl, fallback_y, "[Image Error]")

    render_element_with_rotation(
        canvas_obj, element, x, y, unit_obj, render_image_callback
    )


def extract_entity_fields(entity):
    """Extract all field values from an entity for label generation"""
    entity_data = {}
    fields = entity.get("fields", {})

    for field_name, field_info in fields.items():
        field_value = field_info.get("value", "")
        field_type = field_info.get("type", "")

        if isinstance(field_value, list):
            if (
                field_value
                and isinstance(field_value[0], dict)
                and "id" in field_value[0]
            ):
                if field_type in ["REFERENCE", "INSTANCE_SUBMISSION"]:
                    # Keep entity references for Table elements
                    entity_data[field_name] = field_value
                else:
                    entity_data[field_name] = ", ".join(
                        [ref.get("id", "") for ref in field_value]
                    )
            elif field_value:
                entity_data[field_name] = ", ".join([str(item) for item in field_value])
            else:
                entity_data[field_name] = ""
        elif isinstance(field_value, dict):
            entity_data[field_name] = str(field_value.get("value", field_value))
        else:
            entity_data[field_name] = (
                str(field_value) if field_value is not None else ""
            )

    # Store source entity for property access
    entity_data["_source_entity"] = entity

    return entity_data


def run_preview_mode(config):
    """Generate Labels for testing"""
    preview_entities_refs = config.get("Preview Entities", [])

    if not preview_entities_refs:
        print("No preview entities configured")
        return

    preview_labels = []

    for entity_ref in preview_entities_refs:
        try:
            print(f"Generating preview for entity: {entity_ref['id']}")
            entity = seal.get_entity(entity_id=entity_ref["id"])
            label_file = generate_and_upload_label(entity, config, mode="Preview")
            preview_labels.append(label_file)
            print(f"Preview label created successfully: {label_file['id']}")

        except Exception as e:
            print(f"Failed to generate preview for entity {entity_ref['id']}: {str(e)}")
            continue

    update_preview_references(preview_labels, config)
    print(f"Generated {len(preview_labels)} Labels")


def run_trigger_mode(config, source_entity_id):
    """Generate label for triggering entity"""
    try:
        entity = seal.get_entity(entity_id=source_entity_id)
        entity_data = extract_entity_fields(entity)
        pdf_buffer = create_label_pdf(config, entity_data)
        create_or_update_trigger_label(pdf_buffer, entity, config)
        print(f"Label generated for entity: {entity.get('title')}")

    except Exception as e:
        print(f"Failed to generate trigger label: {str(e)}")
        raise


def run_manual_mode(config, containing_entity_id):
    """Generate label for containing entity and try to update Labels field"""
    try:
        entity = seal.get_entity(entity_id=containing_entity_id)
        label_file = generate_and_upload_label(entity, config, mode="Manual")

        # Try to update Labels field on the containing entity
        try:
            update_manual_labels_preview(containing_entity_id, label_file)
            print(
                f"Manual label generated and added to Labels for entity: {entity.get('title')}"
            )
        except Exception as field_error:
            print(
                f"Manual label generated successfully, but failed to update Labels field: {str(field_error)}"
            )
            print(f"Label file created with ID: {label_file['id']}")

    except Exception as e:
        print(f"Failed to generate manual label: {str(e)}")
        raise


def generate_and_upload_label(entity, config, mode="Preview"):
    """Shared logic to generate PDF and upload label for an entity"""
    entity_data = extract_entity_fields(entity)
    pdf_buffer = create_label_pdf(config, entity_data)
    return upload_pdf_as_label(pdf_buffer, entity, config, mode=mode)


def upload_pdf_as_preview(pdf_buffer, source_entity, config):
    """Upload PDF as preview label file entity"""
    return upload_pdf_as_label(pdf_buffer, source_entity, config, mode="Preview")


def upload_pdf_as_label(pdf_buffer, source_entity, config, mode="Preview"):
    """Upload PDF as label file entity with specified mode prefix"""
    timestamp = str(int(time.time()))

    title_field_value = get_field_or_property_value(source_entity, "title")

    if isinstance(title_field_value, list):
        entity_title = str(title_field_value[0]) if title_field_value else mode
    elif isinstance(title_field_value, dict):
        entity_title = str(title_field_value.get("value", mode))
    elif title_field_value:
        entity_title = str(title_field_value)
    else:
        entity_title = source_entity.get("title", mode)

    filename = f"{mode}_{entity_title}_{timestamp}.pdf"

    temp_filename = f"temp_{mode.lower()}_{timestamp}.pdf"
    with open(temp_filename, "wb") as f:
        f.write(pdf_buffer.read())

    try:
        file_entity = seal.upload_file(
            file_path=temp_filename, file_name=filename, type_title="Label"
        )

        return file_entity

    finally:
        if os.path.exists(temp_filename):
            os.remove(temp_filename)


def update_manual_labels_preview(entity_id, label_file):
    """Update the Labels field on an entity with the generated label"""
    try:
        # Get current entity to check if it's editable
        entity = seal.get_entity(entity_id=entity_id)

        # Make entity editable if it's not already
        if entity["status"] != "EDITABLE":
            seal.make_entity_editable(entity_id)

        # Create reference to the label file
        label_ref = {"id": label_file["id"], "version": None}

        # Get current Labels field value
        current_entity = seal.get_entity(entity_id=entity_id)
        current_labels = (
            current_entity.get("fields", {}).get("Labels", {}).get("value", [])
        )

        # Add new label to the list (or create new list if field doesn't exist)
        if isinstance(current_labels, list):
            updated_labels = current_labels + [label_ref]
        else:
            updated_labels = [label_ref]

        # Update the field
        seal.update_field_value_in_entity(
            entity_id=entity_id,
            field_name="Labels",
            field_value=updated_labels,
        )

        print(f"Successfully updated Labels field with {len(updated_labels)} labels")

    except Exception as e:
        raise Exception(f"Failed to update Labels field: {str(e)}")


def create_or_update_trigger_label(pdf_buffer, source_entity, config):
    """Create or update trigger label (adapted from fixedGenerationScript.py)"""
    source_entity_id = source_entity["id"]

    title_field_value = get_field_or_property_value(source_entity, "title")

    if isinstance(title_field_value, list):
        label_title = str(title_field_value[0]) if title_field_value else "Label"
    elif isinstance(title_field_value, dict):
        label_title = str(title_field_value.get("value", "Label"))
    elif title_field_value:
        label_title = str(title_field_value)
    else:
        label_title = source_entity.get("title", "Label")

    timestamp = str(int(time.time()))
    temp_filename = f"label_{source_entity_id}_{timestamp}.pdf"
    with open(temp_filename, "wb") as f:
        f.write(pdf_buffer.read())

    try:
        search_query = {
            "filters": {
                "fieldValue": [
                    {
                        "name": "Label Source",
                        "operator": "=",
                        "value": f"ref({source_entity_id})",
                    }
                ]
            }
        }

        existing_labels = seal.search_entities(search_query)

        # Use the first matching label by Label Source
        existing_label_id = existing_labels[0]["id"] if existing_labels else None

        if existing_label_id:
            print(f"Updating existing label: {existing_label_id}")
            update_existing_label(existing_label_id, temp_filename, label_title, config)
        else:
            print(f"Creating new label for entity: {source_entity_id}")
            create_new_label(temp_filename, label_title, source_entity_id, config)

    finally:
        if os.path.exists(temp_filename):
            os.remove(temp_filename)


def update_existing_label(label_id, temp_filename, label_title, config):
    """Update existing label with new content"""
    existing_label = seal.get_entity(entity_id=label_id)
    if existing_label["status"] != "EDITABLE":
        seal.make_entity_editable(label_id)

    timestamp = str(int(time.time()))
    unique_filename = f"{label_title}_label_{timestamp}.pdf"

    temp_file_entity = seal.upload_file(
        file_path=temp_filename, file_name=unique_filename, type_title="Label"
    )

    new_file_id = temp_file_entity["content"]["value"]["fileId"]
    new_content = {"type": "FILE", "value": {"fileId": new_file_id}}

    seal._request(
        url=f"entities/{label_id}/content",
        method="PATCH",
        json={"content": new_content},
    )

    current_title = existing_label.get("title")
    if current_title != label_title:
        seal.update_entity_title(label_id, label_title)

    seal.archive_entity(temp_file_entity["id"], True)

    print(f"Label updated successfully: {label_id}")


def create_new_label(temp_filename, label_title, source_entity_id, config):
    """Create new label file entity"""
    # Upload the PDF file
    file_entity = seal.upload_file(
        file_path=temp_filename, file_name=f"{label_title}.pdf", type_title="Label"
    )

    # Update title
    seal.update_entity_title(file_entity["id"], label_title)

    # Set reference fields
    label_entity_id = file_entity["id"]

    # Label Source reference
    seal.update_field_value_in_entity(
        entity_id=label_entity_id,
        field_name="Label Source",
        field_value=[{"id": source_entity_id, "version": None}],
    )

    print(f"New label created successfully: {label_entity_id}")


def update_preview_references(preview_labels, config):
    """Update the Labels reference field in configuration entity"""
    if not preview_labels:
        return

    config_entity_id = config.get("Configuration Entity ID")
    if not config_entity_id:
        print("No configuration entity ID found, cannot update Labels")
        return

    preview_refs = [{"id": label["id"], "version": None} for label in preview_labels]

    try:
        seal.update_field_value_in_entity(
            entity_id=config_entity_id,
            field_name="Labels",
            field_value=preview_refs,
        )
        print(
            f"Updated Labels on configuration entity with {len(preview_refs)} references"
        )
    except Exception as e:
        print(f"Failed to update Labels field: {str(e)}")


def create_label(seal_instance):
    global seal

    seal = seal_instance

    return main()


# Only run main() when script is executed directly, not when imported
if __name__ == "__main__":
    main()

Last updated