"""
Notes management for StarPunk

This module provides CRUD operations for notes with atomic file+database
synchronization. All write operations use database transactions to ensure
files and database records stay in sync.

Functions:
    create_note: Create new note with file and database entry
    get_note: Retrieve note by slug or ID
    list_notes: List notes with filtering and pagination
    update_note: Update note content and/or metadata
    delete_note: Delete note (soft or hard delete)

Exceptions:
    NoteNotFoundError: Note does not exist
    InvalidNoteDataError: Invalid content or parameters
    NoteSyncError: File/database synchronization failure
    NoteError: Base exception for all note operations
"""

# Standard library imports
from datetime import datetime
from pathlib import Path
from typing import Optional

# Third-party imports
from flask import current_app

# Local imports
from starpunk.database import get_db
from starpunk.models import Note
from starpunk.utils import (
    generate_slug,
    make_slug_unique,
    generate_note_path,
    ensure_note_directory,
    write_note_file,
    delete_note_file,
    calculate_content_hash,
    validate_note_path,
    validate_slug,
)


# Custom Exceptions


class NoteError(Exception):
    """Base exception for note operations"""

    pass


class NoteNotFoundError(NoteError):
    """
    Raised when a note cannot be found

    This exception is raised when attempting to retrieve, update, or delete
    a note that doesn't exist in the database.

    Attributes:
        identifier: The slug or ID used to search for the note
        message: Human-readable error message
    """

    def __init__(self, identifier: str | int, message: Optional[str] = None):
        self.identifier = identifier
        if message is None:
            message = f"Note not found: {identifier}"
        super().__init__(message)


class InvalidNoteDataError(NoteError, ValueError):
    """
    Raised when note data is invalid

    This exception is raised when attempting to create or update a note
    with invalid data (empty content, invalid slug, etc.)

    Attributes:
        field: The field that failed validation
        value: The invalid value
        message: Human-readable error message
    """

    def __init__(self, field: str, value: any, message: Optional[str] = None):
        self.field = field
        self.value = value
        if message is None:
            message = f"Invalid {field}: {value}"
        super().__init__(message)


class NoteSyncError(NoteError):
    """
    Raised when file/database synchronization fails

    This exception is raised when a file operation and database operation
    cannot be kept in sync (e.g., file written but database insert failed).

    Attributes:
        operation: The operation that failed ('create', 'update', 'delete')
        details: Additional details about the failure
        message: Human-readable error message
    """

    def __init__(self, operation: str, details: str, message: Optional[str] = None):
        self.operation = operation
        self.details = details
        if message is None:
            message = f"Sync error during {operation}: {details}"
        super().__init__(message)


# Helper Functions


def _get_existing_slugs(db) -> set[str]:
    """
    Query all existing slugs from database

    Args:
        db: Database connection

    Returns:
        Set of existing slug strings
    """
    rows = db.execute("SELECT slug FROM notes").fetchall()
    return {row["slug"] for row in rows}


# Core CRUD Functions


def create_note(
    content: str, published: bool = False, created_at: Optional[datetime] = None, custom_slug: Optional[str] = None
) -> Note:
    """
    Create a new note

    Creates a new note by generating a unique slug, writing the markdown
    content to a file, and inserting a database record. File and database
    operations are atomic - if either fails, both are rolled back.

    Args:
        content: Markdown content for the note (must not be empty)
        published: Whether the note should be published (default: False)
        created_at: Creation timestamp (default: current UTC time)
        custom_slug: Optional custom slug (from Micropub mp-slug property)

    Returns:
        Note object with all metadata and content loaded

    Raises:
        InvalidNoteDataError: If content is empty or whitespace-only
        NoteSyncError: If file write succeeds but database insert fails
        OSError: If file cannot be written (permissions, disk full, etc.)
        ValueError: If configuration is missing or invalid

    Examples:
        >>> # Create unpublished draft
        >>> note = create_note("# My First Note\\n\\nContent here.", published=False)
        >>> print(note.slug)
        'my-first-note'

        >>> # Create published note
        >>> note = create_note(
        ...     "Just published this!",
        ...     published=True
        ... )
        >>> print(note.published)
        True

        >>> # Create with specific timestamp
        >>> from datetime import datetime
        >>> note = create_note(
        ...     "Backdated note",
        ...     created_at=datetime(2024, 1, 1, 12, 0, 0)
        ... )

    Transaction Safety:
        1. Validates content (before any changes)
        2. Generates unique slug (database query)
        3. Writes file to disk
        4. Begins database transaction
        5. Inserts database record
        6. If database fails: deletes file, raises NoteSyncError
        7. If successful: commits transaction, returns Note

    Notes:
        - Slug is generated from first 5 words of content
        - Random suffix added if slug already exists
        - File path follows pattern: data/notes/YYYY/MM/slug.md
        - Content hash calculated and stored for integrity checking
        - created_at and updated_at set to same value initially
    """
    # 1. VALIDATION (before any changes)
    if not content or not content.strip():
        raise InvalidNoteDataError(
            "content", content, "Content cannot be empty or whitespace-only"
        )

    # 2. SETUP
    if created_at is None:
        created_at = datetime.utcnow()

    updated_at = created_at  # Same as created_at for new notes

    data_dir = Path(current_app.config["DATA_PATH"])

    # 3. GENERATE OR VALIDATE SLUG
    # Query all existing slugs from database
    db = get_db(current_app)
    existing_slugs = _get_existing_slugs(db)

    if custom_slug:
        # Use custom slug (from Micropub mp-slug property)
        from starpunk.slug_utils import validate_and_sanitize_custom_slug
        success, slug, error = validate_and_sanitize_custom_slug(custom_slug, existing_slugs)
        if not success:
            raise InvalidNoteDataError("slug", custom_slug, error)
    else:
        # Generate base slug from content
        base_slug = generate_slug(content, created_at)

        # Make unique if collision
        slug = make_slug_unique(base_slug, existing_slugs)

        # Validate final slug (defensive check)
        if not validate_slug(slug):
            raise InvalidNoteDataError("slug", slug, f"Generated slug is invalid: {slug}")

    # 4. GENERATE FILE PATH
    note_path = generate_note_path(slug, created_at, data_dir)

    # Security: Validate path stays within data directory
    if not validate_note_path(note_path, data_dir):
        raise NoteSyncError(
            "create",
            f"Generated path outside data directory: {note_path}",
            "Path validation failed",
        )

    # 5. CALCULATE CONTENT HASH
    content_hash = calculate_content_hash(content)

    # 6. WRITE FILE (before database to fail fast on disk issues)
    try:
        ensure_note_directory(note_path)
        write_note_file(note_path, content)
    except OSError as e:
        # File write failed, nothing to clean up
        raise NoteSyncError(
            "create",
            f"Failed to write file: {e}",
            f"Could not write note file: {note_path}",
        )

    # 7. INSERT DATABASE RECORD (transaction starts here)
    file_path_rel = str(note_path.relative_to(data_dir))

    try:
        db.execute(
            """
            INSERT INTO notes (slug, file_path, published, created_at, updated_at, content_hash)
            VALUES (?, ?, ?, ?, ?, ?)
            """,
            (slug, file_path_rel, published, created_at, updated_at, content_hash),
        )
        db.commit()
    except Exception as e:
        # Database insert failed, delete the file we created
        try:
            note_path.unlink()
        except OSError:
            # Log warning but don't fail - file cleanup is best effort
            current_app.logger.warning(
                f"Failed to clean up file after DB error: {note_path}"
            )

        # Raise sync error
        raise NoteSyncError(
            "create", f"Database insert failed: {e}", f"Failed to create note: {slug}"
        )

    # 8. RETRIEVE AND RETURN NOTE OBJECT
    # Get the auto-generated ID
    note_id = db.execute("SELECT last_insert_rowid()").fetchone()[0]

    # Fetch the complete record
    row = db.execute("SELECT * FROM notes WHERE id = ?", (note_id,)).fetchone()

    # Create Note object
    note = Note.from_row(row, data_dir)

    # 9. UPDATE FTS INDEX (if available)
    try:
        from starpunk.search import update_fts_index, has_fts_table
        db_path = Path(current_app.config["DATABASE_PATH"])
        if has_fts_table(db_path):
            update_fts_index(db, note_id, slug, content)
            db.commit()
    except Exception as e:
        # FTS update failure should not prevent note creation
        current_app.logger.warning(f"Failed to update FTS index for note {slug}: {e}")

    return note


def get_note(
    slug: Optional[str] = None, id: Optional[int] = None, load_content: bool = True
) -> Optional[Note]:
    """
    Get a note by slug or ID

    Retrieves note metadata from database and optionally loads content
    from file. Exactly one of slug or id must be provided.

    Args:
        slug: Note slug (unique identifier in URLs)
        id: Note database ID (primary key)
        load_content: Whether to load file content (default: True)

    Returns:
        Note object with metadata and optionally content, or None if not found

    Raises:
        ValueError: If both slug and id provided, or neither provided
        OSError: If file cannot be read (when load_content=True)
        FileNotFoundError: If note file doesn't exist (when load_content=True)

    Examples:
        >>> # Get by slug
        >>> note = get_note(slug="my-first-note")
        >>> if note:
        ...     print(note.content)  # Content loaded
        ... else:
        ...     print("Note not found")

        >>> # Get by ID
        >>> note = get_note(id=42)

        >>> # Get metadata only (no file I/O)
        >>> note = get_note(slug="my-note", load_content=False)
        >>> print(note.slug)  # Works
        >>> print(note.content)  # Will trigger file load on access

        >>> # Check if note exists
        >>> if get_note(slug="maybe-exists"):
        ...     print("Note exists")

    Performance:
        - Metadata retrieval: Single database query, <1ms
        - Content loading: File I/O, typically <5ms for normal notes
        - Use load_content=False for list operations to avoid file I/O

    Notes:
        - Returns None if note not found (does not raise exception)
        - Content hash verification is optional (logs warning if mismatch)
        - Note.content property will lazy-load if load_content=False
        - Soft-deleted notes (deleted_at != NULL) are excluded
    """
    # 1. VALIDATE PARAMETERS
    if slug is None and id is None:
        raise ValueError("Must provide either slug or id")

    if slug is not None and id is not None:
        raise ValueError("Cannot provide both slug and id")

    # 2. QUERY DATABASE
    db = get_db(current_app)

    if slug is not None:
        # Query by slug
        row = db.execute(
            "SELECT * FROM notes WHERE slug = ? AND deleted_at IS NULL", (slug,)
        ).fetchone()
    else:
        # Query by ID
        row = db.execute(
            "SELECT * FROM notes WHERE id = ? AND deleted_at IS NULL", (id,)
        ).fetchone()

    # 3. CHECK IF FOUND
    if row is None:
        return None

    # 4. CREATE NOTE OBJECT
    data_dir = Path(current_app.config["DATA_PATH"])
    note = Note.from_row(row, data_dir)

    # 5. OPTIONALLY LOAD CONTENT
    if load_content:
        # Access content property to trigger load
        try:
            _ = note.content
        except (FileNotFoundError, OSError) as e:
            current_app.logger.warning(
                f"Failed to load content for note {note.slug}: {e}"
            )

    # 6. OPTIONALLY VERIFY INTEGRITY
    # This is a passive check - log warning but don't fail
    if load_content and note.content_hash:
        try:
            if not note.verify_integrity():
                current_app.logger.warning(
                    f"Content hash mismatch for note {note.slug}. "
                    f"File may have been modified externally."
                )
        except Exception as e:
            current_app.logger.warning(
                f"Failed to verify integrity for note {note.slug}: {e}"
            )

    # 7. RETURN NOTE
    return note


def list_notes(
    published_only: bool = False,
    limit: int = 50,
    offset: int = 0,
    order_by: str = "created_at",
    order_dir: str = "DESC",
) -> list[Note]:
    """
    List notes with filtering and pagination

    Retrieves notes from database with optional filtering by published
    status, sorting, and pagination. Does not load file content for
    performance - use note.content to lazy-load when needed.

    Args:
        published_only: If True, only return published notes (default: False)
        limit: Maximum number of notes to return (default: 50, max: 1000)
        offset: Number of notes to skip for pagination (default: 0)
        order_by: Field to sort by (default: 'created_at')
        order_dir: Sort direction, 'ASC' or 'DESC' (default: 'DESC')

    Returns:
        List of Note objects with metadata only (content not loaded)

    Raises:
        ValueError: If order_by is not a valid column name (SQL injection prevention)
        ValueError: If order_dir is not 'ASC' or 'DESC'
        ValueError: If limit exceeds maximum allowed value

    Examples:
        >>> # List recent published notes
        >>> notes = list_notes(published_only=True, limit=10)
        >>> for note in notes:
        ...     print(note.slug, note.created_at)

        >>> # List all notes, oldest first
        >>> notes = list_notes(order_dir='ASC')

        >>> # Pagination (page 2, 20 per page)
        >>> notes = list_notes(limit=20, offset=20)

        >>> # List by update time
        >>> notes = list_notes(order_by='updated_at')

    Performance:
        - Single database query
        - No file I/O (content not loaded)
        - Efficient for large result sets with pagination
        - Typical query time: <10ms for 1000s of notes

    Pagination Example:
        >>> page = 1
        >>> per_page = 20
        >>> notes = list_notes(
        ...     published_only=True,
        ...     limit=per_page,
        ...     offset=(page - 1) * per_page
        ... )

    Notes:
        - Excludes soft-deleted notes (deleted_at IS NULL)
        - Content is lazy-loaded when accessed via note.content
        - order_by values are validated to prevent SQL injection
        - Default sort is newest first (created_at DESC)
    """
    # 1. VALIDATE PARAMETERS
    # Prevent SQL injection - validate order_by column
    ALLOWED_ORDER_FIELDS = ["id", "slug", "created_at", "updated_at", "published"]
    if order_by not in ALLOWED_ORDER_FIELDS:
        raise ValueError(
            f"Invalid order_by field: {order_by}. "
            f"Allowed: {', '.join(ALLOWED_ORDER_FIELDS)}"
        )

    # Validate order direction
    order_dir = order_dir.upper()
    if order_dir not in ["ASC", "DESC"]:
        raise ValueError(f"Invalid order_dir: {order_dir}. Must be 'ASC' or 'DESC'")

    # Validate limit (prevent excessive queries)
    MAX_LIMIT = 1000
    if limit > MAX_LIMIT:
        raise ValueError(f"Limit {limit} exceeds maximum {MAX_LIMIT}")

    if limit < 1:
        raise ValueError("Limit must be >= 1")

    if offset < 0:
        raise ValueError("Offset must be >= 0")

    # 2. BUILD QUERY
    # Start with base query
    query = "SELECT * FROM notes WHERE deleted_at IS NULL"

    # Add filters
    params = []
    if published_only:
        query += " AND published = 1"

    # Add ordering (safe because order_by validated above)
    query += f" ORDER BY {order_by} {order_dir}"

    # Add pagination
    query += " LIMIT ? OFFSET ?"
    params.extend([limit, offset])

    # 3. EXECUTE QUERY
    db = get_db(current_app)
    rows = db.execute(query, params).fetchall()

    # 4. CREATE NOTE OBJECTS (without loading content)
    data_dir = Path(current_app.config["DATA_PATH"])
    notes = [Note.from_row(row, data_dir) for row in rows]

    return notes


def update_note(
    slug: Optional[str] = None,
    id: Optional[int] = None,
    content: Optional[str] = None,
    published: Optional[bool] = None,
) -> Note:
    """
    Update a note's content and/or published status

    Updates note content and/or metadata, maintaining atomic synchronization
    between file and database. At least one of content or published must
    be provided.

    Args:
        slug: Note slug to update (mutually exclusive with id)
        id: Note ID to update (mutually exclusive with slug)
        content: New markdown content (None = no change)
        published: New published status (None = no change)

    Returns:
        Updated Note object with new content and metadata

    Raises:
        ValueError: If both slug and id provided, or neither provided
        ValueError: If neither content nor published provided (no changes)
        NoteNotFoundError: If note doesn't exist
        InvalidNoteDataError: If content is empty/whitespace (when provided)
        NoteSyncError: If file update succeeds but database update fails
        OSError: If file cannot be written

    Examples:
        >>> # Update content only
        >>> note = update_note(
        ...     slug="my-note",
        ...     content="# Updated content\\n\\nNew text here."
        ... )

        >>> # Publish a draft
        >>> note = update_note(slug="draft-note", published=True)

        >>> # Update both content and status
        >>> note = update_note(
        ...     id=42,
        ...     content="New content",
        ...     published=True
        ... )

        >>> # Unpublish a note
        >>> note = update_note(slug="old-post", published=False)

    Transaction Safety:
        1. Validates parameters
        2. Retrieves existing note from database
        3. If content changed: writes new file (old file preserved)
        4. Begins database transaction
        5. Updates database record
        6. If database fails: log error, raise NoteSyncError
        7. If successful: commits transaction, returns updated Note

    Notes:
        - Slug cannot be changed (use delete + create for that)
        - updated_at is automatically set to current time
        - Content hash recalculated if content changes
        - File is overwritten atomically (temp file + rename)
        - Old file content is lost (no backup by default)
    """
    # 1. VALIDATE PARAMETERS
    if slug is None and id is None:
        raise ValueError("Must provide either slug or id")

    if slug is not None and id is not None:
        raise ValueError("Cannot provide both slug and id")

    if content is None and published is None:
        raise ValueError("Must provide at least one of content or published to update")

    # Validate content if provided
    if content is not None:
        if not content or not content.strip():
            raise InvalidNoteDataError(
                "content", content, "Content cannot be empty or whitespace-only"
            )

    # 2. GET EXISTING NOTE
    existing_note = get_note(slug=slug, id=id, load_content=False)

    if existing_note is None:
        identifier = slug if slug is not None else id
        raise NoteNotFoundError(identifier)

    # 3. SETUP
    updated_at = datetime.utcnow()
    data_dir = Path(current_app.config["DATA_PATH"])
    note_path = data_dir / existing_note.file_path

    # Validate path (security check)
    if not validate_note_path(note_path, data_dir):
        raise NoteSyncError(
            "update",
            f"Note file path outside data directory: {note_path}",
            "Path validation failed",
        )

    # 4. UPDATE FILE (if content changed)
    new_content_hash = existing_note.content_hash
    if content is not None:
        try:
            # Write new content atomically
            write_note_file(note_path, content)

            # Calculate new hash
            new_content_hash = calculate_content_hash(content)
        except OSError as e:
            raise NoteSyncError(
                "update",
                f"Failed to write file: {e}",
                f"Could not update note file: {note_path}",
            )

    # 5. UPDATE DATABASE
    db = get_db(current_app)

    # Build update query based on what changed
    update_fields = ["updated_at = ?"]
    params = [updated_at]

    if content is not None:
        update_fields.append("content_hash = ?")
        params.append(new_content_hash)

    if published is not None:
        update_fields.append("published = ?")
        params.append(published)

    # Add WHERE clause parameter
    if slug is not None:
        where_clause = "slug = ?"
        params.append(slug)
    else:
        where_clause = "id = ?"
        params.append(id)

    query = f"UPDATE notes SET {', '.join(update_fields)} WHERE {where_clause}"

    try:
        db.execute(query, params)
        db.commit()
    except Exception as e:
        # Database update failed
        # File has been updated, but we can't roll that back easily
        # Log error and raise
        current_app.logger.error(
            f"Database update failed for note {existing_note.slug}: {e}"
        )
        raise NoteSyncError(
            "update",
            f"Database update failed: {e}",
            f"Failed to update note: {existing_note.slug}",
        )

    # 6. UPDATE FTS INDEX (if available and content changed)
    if content is not None:
        try:
            from starpunk.search import update_fts_index, has_fts_table
            db_path = Path(current_app.config["DATABASE_PATH"])
            if has_fts_table(db_path):
                update_fts_index(db, existing_note.id, existing_note.slug, content)
                db.commit()
        except Exception as e:
            # FTS update failure should not prevent note update
            current_app.logger.warning(f"Failed to update FTS index for note {existing_note.slug}: {e}")

    # 7. RETURN UPDATED NOTE
    updated_note = get_note(slug=existing_note.slug, load_content=True)

    return updated_note


def delete_note(
    slug: Optional[str] = None, id: Optional[int] = None, soft: bool = True
) -> None:
    """
    Delete a note (soft or hard delete)

    Deletes a note either by marking it as deleted (soft delete) or by
    permanently removing the file and database record (hard delete).

    Args:
        slug: Note slug to delete (mutually exclusive with id)
        id: Note ID to delete (mutually exclusive with id)
        soft: If True, soft delete (mark deleted_at); if False, hard delete (default: True)

    Returns:
        None

    Raises:
        ValueError: If both slug and id provided, or neither provided
        NoteSyncError: If file deletion succeeds but database update fails
        OSError: If file cannot be deleted

    Examples:
        >>> # Soft delete (default)
        >>> delete_note(slug="old-note")
        >>> # Note marked as deleted, file remains

        >>> # Hard delete
        >>> delete_note(slug="spam-note", soft=False)
        >>> # Note and file permanently removed

        >>> # Delete by ID
        >>> delete_note(id=42, soft=False)

    Soft Delete:
        - Sets deleted_at timestamp in database
        - File remains on disk (optionally moved to .trash/)
        - Note excluded from normal queries (deleted_at IS NULL)
        - Can be undeleted by clearing deleted_at (future feature)

    Hard Delete:
        - Removes database record permanently
        - Deletes file from disk
        - Cannot be recovered
        - Use for spam, test data, or confirmed deletions

    Transaction Safety:
        Soft delete:
        1. Updates database (sets deleted_at)
        2. Optionally moves file to .trash/
        3. If move fails: log warning but succeed (database is source of truth)

        Hard delete:
        1. Deletes database record
        2. Deletes file from disk
        3. If file delete fails: log warning but succeed (record already gone)

    Notes:
        - Soft delete is default and recommended
        - Hard delete is permanent and cannot be undone
        - Missing files during hard delete are not errors (idempotent)
        - Deleting already-deleted note returns successfully (idempotent)
    """
    # 1. VALIDATE PARAMETERS
    if slug is None and id is None:
        raise ValueError("Must provide either slug or id")

    if slug is not None and id is not None:
        raise ValueError("Cannot provide both slug and id")

    # 2. GET EXISTING NOTE
    # For soft delete, exclude already soft-deleted notes
    # For hard delete, get note even if soft-deleted
    if soft:
        existing_note = get_note(slug=slug, id=id, load_content=False)
    else:
        # Hard delete: query including soft-deleted notes
        db = get_db(current_app)
        if slug is not None:
            row = db.execute("SELECT * FROM notes WHERE slug = ?", (slug,)).fetchone()
        else:
            row = db.execute("SELECT * FROM notes WHERE id = ?", (id,)).fetchone()

        if row is None:
            existing_note = None
        else:
            data_dir = Path(current_app.config["DATA_PATH"])
            existing_note = Note.from_row(row, data_dir)

    # 3. CHECK IF NOTE EXISTS
    if existing_note is None:
        # Note not found - could already be deleted
        # For idempotency, don't raise error - just return
        return

    # 4. SETUP
    data_dir = Path(current_app.config["DATA_PATH"])
    note_path = data_dir / existing_note.file_path

    # Validate path (security check)
    if not validate_note_path(note_path, data_dir):
        raise NoteSyncError(
            "delete",
            f"Note file path outside data directory: {note_path}",
            "Path validation failed",
        )

    # 5. PERFORM DELETION
    db = get_db(current_app)

    if soft:
        # SOFT DELETE: Mark as deleted in database
        deleted_at = datetime.utcnow()

        try:
            db.execute(
                "UPDATE notes SET deleted_at = ? WHERE id = ?",
                (deleted_at, existing_note.id),
            )
            db.commit()
        except Exception as e:
            raise NoteSyncError(
                "delete",
                f"Database update failed: {e}",
                f"Failed to soft delete note: {existing_note.slug}",
            )

        # Optionally move file to trash (best effort)
        # This is optional and failure is not critical
        try:
            delete_note_file(note_path, soft=True, data_dir=data_dir)
        except Exception as e:
            current_app.logger.warning(
                f"Failed to move file to trash for note {existing_note.slug}: {e}"
            )
            # Don't fail - database update succeeded

    else:
        # HARD DELETE: Remove from database and filesystem
        try:
            db.execute("DELETE FROM notes WHERE id = ?", (existing_note.id,))
            db.commit()
        except Exception as e:
            raise NoteSyncError(
                "delete",
                f"Database delete failed: {e}",
                f"Failed to delete note: {existing_note.slug}",
            )

        # Delete file (best effort)
        try:
            delete_note_file(note_path, soft=False)
        except FileNotFoundError:
            # File already gone - that's fine
            current_app.logger.info(
                f"File already deleted for note {existing_note.slug}"
            )
        except Exception as e:
            current_app.logger.warning(
                f"Failed to delete file for note {existing_note.slug}: {e}"
            )
            # Don't fail - database record already deleted

    # 6. RETURN (no value)
    return None
