"""
DDL Object Removal Tool
=======================

This script removes database objects from a SQL DDL file based on objects that were matched
in the cross-reference analysis. It produces a "cleaned" DDL containing only objects that
don't have corresponding standalone SQL files in the codebase.

Purpose:
--------
- Remove matched database objects from a DDL file
- Create a cleaned DDL containing only objects without standalone SQL files
- Complement the workflow started by extract_sql_objects.py and cross_reference_sql_objects.py
- Reduce DDL file size by removing redundant objects that exist as individual files

Use Case:
---------
This script is useful when you want to:
- Maintain only objects that don't have individual SQL files in the master DDL
- Reduce DDL file size by removing redundant objects
- Create a "core" DDL with only shared/generic objects
- Prepare DDL for deployment where standalone files are managed separately
- Identify which objects are truly unique to the master DDL vs. maintained elsewhere

Workflow:
---------
1. Load cross-reference JSON from cross_reference_sql_objects.py
2. Auto-pair packages (add bodies when specs are matched, and vice versa)
3. Parse the DDL file to identify all CREATE statements
4. For each object, check if it's in the matched set
5. Remove matched objects, retain unmatched ones
6. Write cleaned DDL to output file
7. Validate output for orphaned packages

Statement Boundary Detection:
-----------------------------
The script identifies where each CREATE statement ends by looking for specific terminators
based on the object type:

- PACKAGE/PACKAGE BODY: Ends with `END;` on a standalone line
- FUNCTION: Ends with `END$function$` followed by `;` on the next line
- PROCEDURE: Ends with `END$procedure$` followed by `;` on the next line
- TRIGGER: Ends with `END$trigger$` followed by `;` on the next line

This allows the script to accurately extract complete object definitions from the DDL,
regardless of their length or complexity.

Auto-Pairing Behavior:
---------------------
To prevent orphaned package objects (which would cause compilation errors), the script
automatically pairs package specifications with their bodies:

- When a PACKAGE spec is in the removal list, its PACKAGE BODY is automatically added
- When a PACKAGE BODY is in the removal list, its PACKAGE spec is automatically added
- This ensures packages are always removed as complete units
- The pairing happens automatically and is reported in the console output

Example: If the cross-reference JSON contains only "overnight_pkg" in packages (not in
package_bodies), the script will automatically add "overnight_pkg" to the removal list
for package bodies as well, preventing an orphaned body in the cleaned DDL.

Output Format:
--------------
The cleaned DDL file maintains:
- Original formatting and indentation
- UTF-8 encoding
- Comments and blank lines between statements
- All non-CREATE content (table definitions, views, etc.)
- Only CREATE statements for objects NOT in the matched set are removed
- No orphaned package objects (validated automatically)

Usage:
------
Basic usage with defaults:
    python remove_matched_objects.py

With custom files:
    python remove_matched_objects.py -d ddl.sql -i matched.json -o cleaned.sql

Dry-run mode (preview what would be removed):
    python remove_matched_objects.py --dry-run

With verbose output:
    python remove_matched_objects.py --verbose

Complete workflow example:
    python extract_sql_objects.py -i SQL-Golden-Axium-DDL.ddl -o extracted.json
    python cross_reference_sql_objects.py -i extracted.json -o matched.json
    python remove_matched_objects.py -d SQL-Golden-Axium-DDL.ddl -i matched.json -o cleaned.ddl

Command-line Arguments:
-----------------------
-d, --ddl       : Path to the input SQL DDL file (default: SQL-Golden-Axium-DDL.ddl)
-i, --input     : Path to cross-reference JSON file from cross_reference_sql_objects.py 
                  (default: matched_sql_objects.json)
-o, --output    : Path to output cleaned DDL file (default: cleaned_ddl.sql)
--dry-run       : Preview mode - show what would be removed without creating output file
--verbose       : Show detailed progress information during processing

Technical Details:
------------------
- Uses regex pattern matching for CREATE OR REPLACE statements (same as extract_sql_objects.py)
- Line-by-line parsing with state machine for boundary detection
- Case-insensitive object name matching for consistency
- UTF-8 encoding support for international characters
- Handles all three detail levels from cross_reference_sql_objects.py output (minimal/standard/full)
- Memory-efficient processing (processes DDL line by line without loading entire file)
- Preserves all content outside of matched CREATE statements
- Automatic package pairing to prevent orphaned objects
- Post-processing validation to ensure DDL integrity

Integration with Other Scripts:
--------------------------------
This script is Part 3 of a three-script workflow:

Part 1: extract_sql_objects.py
  - Extracts all database objects from the master DDL file
  - Outputs: extracted_sql_objects.json

Part 2: cross_reference_sql_objects.py
  - Scans directory tree for standalone SQL files
  - Matches them against objects from the master DDL
  - Outputs: matched_sql_objects.json

Part 3: remove_matched_objects.py (this script)
  - Removes matched objects from the master DDL
  - Outputs: cleaned_ddl.sql (contains only unmatched objects)

Author: Generated for Axium Services
Date: 2026-01-23
"""

import json
import re
import argparse
from pathlib import Path
from datetime import datetime


def load_matched_objects(json_file, verbose=False):
    """
    Load and parse cross-reference JSON, return set of (type, name) tuples.
    
    Handles all three detail levels from cross_reference_sql_objects.py:
    - minimal: matched_objects contains lists of strings
    - standard: matched_objects contains lists of dicts with 'name' key
    - full: matched_objects contains lists of dicts with 'name' and 'files' keys
    
    Args:
        json_file: Path to the cross-reference JSON file
        verbose: If True, print detailed information
    
    Returns:
        Set of tuples: {(object_type, object_name_lower), ...}
    
    Raises:
        FileNotFoundError: If JSON file doesn't exist
        json.JSONDecodeError: If JSON is malformed
        KeyError: If expected structure is missing
    """
    if verbose:
        print(f"  Reading JSON file: {json_file}")
    
    # Load JSON file
    with open(json_file, 'r', encoding='utf-8') as f:
        data = json.load(f)
    
    # Validate structure
    if 'matched_objects' not in data:
        raise KeyError("JSON file missing 'matched_objects' key")
    
    matched_objects = data['matched_objects']
    matched_set = set()
    
    # Map JSON keys to object types (for case-insensitive matching)
    type_mapping = {
        'packages': 'PACKAGE',
        'package_bodies': 'PACKAGE BODY',
        'functions': 'FUNCTION',
        'procedures': 'PROCEDURE',
        'triggers': 'TRIGGER'
    }
    
    # Process each object type
    for json_key, object_type in type_mapping.items():
        if json_key not in matched_objects:
            continue
        
        obj_list = matched_objects[json_key]
        
        for obj in obj_list:
            # Handle different detail levels
            if isinstance(obj, str):
                # Minimal mode: objects are strings
                object_name = obj
            elif isinstance(obj, dict):
                # Standard/Full mode: objects are dicts with 'name' key
                object_name = obj.get('name', '')
            else:
                if verbose:
                    print(f"  [WARNING] Unexpected object format: {obj}")
                continue
            
            if object_name:
                # Store as (type, name_lower) tuple for case-insensitive matching
                matched_set.add((object_type, object_name.lower()))
                
                if verbose:
                    print(f"  Loaded: {object_type} {object_name}")
    
    return matched_set


def auto_pair_packages(matched_set, verbose=False):
    """
    Automatically add package bodies when specs are matched, and vice versa.
    This prevents orphaned objects in the cleaned DDL.
    
    When a PACKAGE specification is in the matched set, its corresponding PACKAGE BODY
    is automatically added (and vice versa). This ensures that packages are always
    removed as complete units, preventing compilation errors in the cleaned DDL.
    
    Args:
        matched_set: Set of (object_type, object_name_lower) tuples
        verbose: If True, print detailed information about pairing
    
    Returns:
        Expanded set with paired packages/bodies added
    """
    expanded_set = matched_set.copy()
    added_pairs = []
    
    for obj_type, obj_name in matched_set:
        if obj_type == 'PACKAGE':
            # If package spec is matched, also match its body
            body_tuple = ('PACKAGE BODY', obj_name)
            if body_tuple not in expanded_set:
                expanded_set.add(body_tuple)
                added_pairs.append(f"PACKAGE BODY {obj_name} (paired with spec)")
        elif obj_type == 'PACKAGE BODY':
            # If package body is matched, also match its spec
            spec_tuple = ('PACKAGE', obj_name)
            if spec_tuple not in expanded_set:
                expanded_set.add(spec_tuple)
                added_pairs.append(f"PACKAGE {obj_name} (paired with body)")
    
    if verbose and added_pairs:
        print("  Auto-paired packages to prevent orphans:")
        for pair in added_pairs:
            print(f"    + {pair}")
    
    return expanded_set


def find_statement_end(lines, start_idx, object_type, verbose=False):
    """
    Find the end line index for a CREATE statement.
    
    Different object types have different terminators:
    - PACKAGE/PACKAGE BODY: END; on standalone line
    - FUNCTION: END$function$ followed by ; on next line
    - PROCEDURE: END$procedure$ followed by ; on next line
    - TRIGGER: END$trigger$ followed by ; on next line
    
    Args:
        lines: List of file lines
        start_idx: Starting line index to search from
        object_type: Type of object (PACKAGE, FUNCTION, etc.)
        verbose: If True, print detailed information
    
    Returns:
        Line index where the statement ends, or None if not found
    """
    if object_type in ['PACKAGE', 'PACKAGE BODY']:
        # Look for "END;" on a standalone line
        # For PACKAGE BODY, we find the next CREATE statement as the boundary
        # to avoid issues with nested BEGIN/END blocks in functions/procedures
        
        if object_type == 'PACKAGE BODY':
            # For PACKAGE BODY, find the next CREATE statement as the boundary
            # Package bodies can have complex nested structures with BEGIN/END blocks
            # in functions, procedures, and initialization sections
            # This approach is more reliable than counting BEGIN/END pairs
            
            for i in range(start_idx, len(lines)):
                line_stripped = lines[i].strip()
                line_upper = line_stripped.upper()
                
                # Skip comments
                if not line_stripped or line_stripped.startswith('--'):
                    continue
                
                # Look for the next CREATE statement (start of next object)
                if line_upper.startswith('CREATE ') and i > start_idx:
                    # The package body ends at the line before this CREATE
                    # Walk backwards to find the actual END; statement
                    for j in range(i - 1, start_idx, -1):
                        j_stripped = lines[j].strip().upper()
                        if j_stripped in ['END;', 'END']:
                            if verbose:
                                print(f"    Found package body END at line {j + 1} (before next CREATE at {i + 1})")
                            return j
                    # If no END found, return the line before CREATE
                    if verbose:
                        print(f"    No END found, returning line before CREATE at {i + 1}")
                    return i - 1
            
            # If no next CREATE found, search for END; from the end of file backwards
            # This handles the case where the package body is the last object in the file
            for i in range(len(lines) - 1, start_idx, -1):
                i_stripped = lines[i].strip().upper()
                if i_stripped in ['END;', 'END']:
                    if verbose:
                        print(f"    Found package body END at line {i + 1} (EOF)")
                    return i
        else:
            # For PACKAGE (specification), just look for the first END;
            # Package specs typically don't have BEGIN blocks, only declarations
            for i in range(start_idx, len(lines)):
                if lines[i].strip() == 'END;':
                    return i
    else:
        # FUNCTION, PROCEDURE, TRIGGER: Use the same strategy as PACKAGE BODY
        # Find the next CREATE statement, then walk backwards to find the last semicolon
        # This is more reliable than trying to parse SQL syntax
        
        for i in range(start_idx, len(lines)):
            line_stripped = lines[i].strip()
            line_upper = line_stripped.upper()
            
            # Skip comments
            if not line_stripped or line_stripped.startswith('--'):
                continue
            
            # Look for the next CREATE statement (start of next object)
            if line_upper.startswith('CREATE ') and i > start_idx:
                # The current object ends at the line before this CREATE
                # Walk backwards to find the actual semicolon
                for j in range(i - 1, start_idx, -1):
                    j_stripped = lines[j].strip()
                    if ';' in j_stripped:
                        if verbose:
                            print(f"    Found closing delimiter at line {j + 1}: {j_stripped}")
                        return j
                # If no semicolon found, return the line before CREATE
                if verbose:
                    print(f"    No semicolon found, returning line before CREATE at {i + 1}")
                return i - 1
        
        # If no next CREATE found, search for semicolon from the end of file backwards
        # This handles the case where the object is the last one in the file
        for i in range(len(lines) - 1, start_idx, -1):
            i_stripped = lines[i].strip()
            if ';' in i_stripped and i > start_idx:
                if verbose:
                    print(f"    Found closing delimiter at line {i + 1} (EOF): {i_stripped}")
                return i
    
    # Statement end not found
    if verbose:
        print(f"  [WARNING] Could not find end for {object_type} starting at line {start_idx + 1}")
    return None


def check_for_orphans(ddl_file, verbose=False):
    """
    Validate that the DDL file has no orphaned package objects.
    
    An orphan is:
    - A PACKAGE BODY without its corresponding PACKAGE specification
    - A PACKAGE specification without its corresponding PACKAGE BODY
    
    Args:
        ddl_file: Path to DDL file to validate
        verbose: If True, print detailed information
    
    Returns:
        Tuple of (has_orphans: bool, orphan_list: list)
        orphan_list contains tuples of (object_type, object_name, issue_description)
    """
    if verbose:
        print(f"  Validating DDL for orphaned packages: {ddl_file}")
    
    # Regex pattern to find packages and bodies
    pattern = re.compile(
        r'^CREATE\s+(?:OR\s+REPLACE\s+)?(PACKAGE\s+BODY|PACKAGE)\s+([^\s(]+)',
        re.IGNORECASE | re.MULTILINE
    )
    
    try:
        with open(ddl_file, 'r', encoding='utf-8') as f:
            content = f.read()
    except FileNotFoundError:
        raise FileNotFoundError(f"DDL file not found: {ddl_file}")
    
    # Find all packages and bodies
    packages = set()
    bodies = set()
    
    for match in pattern.finditer(content):
        obj_type = match.group(1).upper().strip()
        obj_name = match.group(2).strip().lower()
        
        if obj_type == 'PACKAGE':
            packages.add(obj_name)
        elif obj_type == 'PACKAGE BODY':
            bodies.add(obj_name)
    
    # Check for orphans
    orphans = []
    
    # Bodies without specs
    for body_name in bodies:
        if body_name not in packages:
            orphans.append(('PACKAGE BODY', body_name, 'body exists without specification'))
    
    # Specs without bodies
    for pkg_name in packages:
        if pkg_name not in bodies:
            orphans.append(('PACKAGE', pkg_name, 'specification exists without body'))
    
    if verbose:
        if orphans:
            print(f"  Found {len(orphans)} orphaned package(s)")
        else:
            print(f"  No orphaned packages found")
    
    return len(orphans) > 0, orphans


def process_ddl(ddl_file, matched_set, output_file, dry_run=False, verbose=False):
    """
    Main processing function to parse DDL and remove matched objects.
    
    Args:
        ddl_file: Path to input DDL file
        matched_set: Set of (object_type, object_name_lower) tuples to remove
        output_file: Path to output cleaned DDL file
        dry_run: If True, don't write output file, just report
        verbose: If True, show detailed progress
    
    Returns:
        Dictionary with statistics about the operation
    """
    # Regex pattern to match CREATE statements (same as extract_sql_objects.py)
    pattern = re.compile(
        r'^CREATE\s+(?:OR\s+REPLACE\s+)?(PACKAGE\s+BODY|PACKAGE|FUNCTION|PROCEDURE|TRIGGER)\s+([^\s(]+)',
        re.IGNORECASE | re.MULTILINE
    )
    
    # Statistics tracking
    stats = {
        'total_objects': 0,
        'removed_objects': 0,
        'retained_objects': 0,
        'removed_packages': 0,
        'removed_package_bodies': 0,
        'removed_functions': 0,
        'removed_procedures': 0,
        'removed_triggers': 0,
        'retained_packages': 0,
        'retained_package_bodies': 0,
        'retained_functions': 0,
        'retained_procedures': 0,
        'retained_triggers': 0
    }
    
    if verbose:
        print(f"  Reading DDL file: {ddl_file}")
    
    # Read the entire DDL file
    try:
        with open(ddl_file, 'r', encoding='utf-8') as f:
            lines = f.readlines()
    except FileNotFoundError:
        raise FileNotFoundError(f"DDL file not found: {ddl_file}")
    
    if verbose:
        print(f"  Total lines in DDL: {len(lines)}")
    
    # Open output file if not dry-run
    output_handle = None
    if not dry_run:
        output_handle = open(output_file, 'w', encoding='utf-8')
    
    try:
        i = 0
        while i < len(lines):
            line = lines[i]
            
            # Check if this line starts a CREATE statement
            match = pattern.match(line)
            
            if match:
                object_type = match.group(1).upper().strip()
                object_name = match.group(2).strip()
                object_name_lower = object_name.lower()
                
                stats['total_objects'] += 1
                
                # Find the end of this statement
                end_idx = find_statement_end(lines, i + 1, object_type, verbose)
                
                if end_idx is None:
                    # Could not find end - skip to next CREATE statement to avoid skipping valid objects
                    if verbose:
                        print(f"  [WARNING] Skipping {object_type} {object_name} - could not find statement end")
                    
                    # Find the next CREATE statement to know where this object likely ends
                    next_create_idx = None
                    for j in range(i + 1, len(lines)):
                        if re.match(r'^CREATE\s+(?:OR\s+REPLACE\s+)?(?:PACKAGE\s+BODY|PACKAGE|FUNCTION|PROCEDURE|TRIGGER)\s+', lines[j], re.IGNORECASE):
                            next_create_idx = j
                            break
                    
                    if next_create_idx is not None:
                        # Skip to the next CREATE statement
                        if verbose:
                            print(f"    Skipping to next CREATE at line {next_create_idx + 1}")
                        i = next_create_idx
                    else:
                        # No more CREATE statements, skip to end of file
                        if verbose:
                            print(f"    No more CREATE statements found, skipping to EOF")
                        break
                    
                    continue
                
                # Check if this object should be removed
                if (object_type, object_name_lower) in matched_set:
                    # Remove this object
                    stats['removed_objects'] += 1
                    
                    # Update type-specific counters
                    if object_type == 'PACKAGE':
                        stats['removed_packages'] += 1
                    elif object_type == 'PACKAGE BODY':
                        stats['removed_package_bodies'] += 1
                    elif object_type == 'FUNCTION':
                        stats['removed_functions'] += 1
                    elif object_type == 'PROCEDURE':
                        stats['removed_procedures'] += 1
                    elif object_type == 'TRIGGER':
                        stats['removed_triggers'] += 1
                    
                    if verbose:
                        print(f"  [REMOVED] {object_type} {object_name} (lines {i + 1}-{end_idx + 1})")
                    
                    # Skip to the line after the statement end
                    i = end_idx + 1
                else:
                    # Retain this object
                    stats['retained_objects'] += 1
                    
                    # Update type-specific counters
                    if object_type == 'PACKAGE':
                        stats['retained_packages'] += 1
                    elif object_type == 'PACKAGE BODY':
                        stats['retained_package_bodies'] += 1
                    elif object_type == 'FUNCTION':
                        stats['retained_functions'] += 1
                    elif object_type == 'PROCEDURE':
                        stats['retained_procedures'] += 1
                    elif object_type == 'TRIGGER':
                        stats['retained_triggers'] += 1
                    
                    if verbose:
                        print(f"  [RETAINED] {object_type} {object_name} (lines {i + 1}-{end_idx + 1})")
                    
                    # Write the entire statement to output
                    if output_handle:
                        for j in range(i, end_idx + 1):
                            output_handle.write(lines[j])
                    
                    # Move to the line after the statement end
                    i = end_idx + 1
            else:
                # Not a CREATE statement - write as-is
                if output_handle:
                    output_handle.write(line)
                i += 1
    
    finally:
        # Close output file if it was opened
        if output_handle:
            output_handle.close()
    
    return stats


def main():
    """Parse arguments and orchestrate the removal process."""
    parser = argparse.ArgumentParser(
        description='Remove matched database objects from a DDL file',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  Basic usage:
    python remove_matched_objects.py
  
  Custom files:
    python remove_matched_objects.py -d ddl.sql -i matched.json -o cleaned.sql
  
  Dry-run (preview only):
    python remove_matched_objects.py --dry-run --verbose
  
  Complete workflow:
    python extract_sql_objects.py -i SQL-Golden-Axium-DDL.ddl -o extracted.json
    python cross_reference_sql_objects.py -i extracted.json -o matched.json
    python remove_matched_objects.py -d SQL-Golden-Axium-DDL.ddl -i matched.json -o cleaned.ddl
        """
    )
    
    parser.add_argument('-d', '--ddl',
                        default='SQL-Golden-Axium-DDL.ddl',
                        help='Input DDL file path (default: SQL-Golden-Axium-DDL.ddl)')
    parser.add_argument('-i', '--input',
                        default='matched_sql_objects.json',
                        help='Cross-reference JSON file from cross_reference_sql_objects.py (default: matched_sql_objects.json)')
    parser.add_argument('-o', '--output',
                        default='cleaned_ddl.sql',
                        help='Output cleaned DDL file path (default: cleaned_ddl.sql)')
    parser.add_argument('--dry-run',
                        action='store_true',
                        help='Show what would be removed without creating output file')
    parser.add_argument('--verbose',
                        action='store_true',
                        help='Show detailed progress information')
    
    args = parser.parse_args()
    
    print("=" * 60)
    print("DDL OBJECT REMOVAL TOOL")
    print("=" * 60)
    print(f"Input DDL:  {args.ddl}")
    print(f"Input JSON: {args.input}")
    print(f"Output DDL: {args.output}")
    if args.dry_run:
        print("Mode:       DRY-RUN (no output file will be created)")
    print("=" * 60)
    print()
    
    try:
        # Load matched objects from JSON
        print("Loading matched objects from JSON...")
        matched_set = load_matched_objects(args.input, args.verbose)
        print(f"Loaded {len(matched_set)} matched objects to remove")
        
        # Auto-pair packages to prevent orphans
        original_count = len(matched_set)
        matched_set = auto_pair_packages(matched_set, args.verbose)
        added_count = len(matched_set) - original_count
        
        if added_count > 0:
            print(f"Auto-paired {added_count} package spec(s)/body(ies) to prevent orphans")
            print(f"Total objects to remove: {len(matched_set)}")
        print()
        
        # Process DDL file
        print("Processing DDL file...")
        stats = process_ddl(args.ddl, matched_set, args.output, args.dry_run, args.verbose)
        
        # Print statistics
        print()
        print("=" * 60)
        print("REMOVAL SUMMARY")
        print("=" * 60)
        print(f"Total objects found:     {stats['total_objects']}")
        print(f"Objects removed:         {stats['removed_objects']}")
        print(f"Objects retained:        {stats['retained_objects']}")
        print()
        print("Removed by type:")
        print(f"  Packages:              {stats['removed_packages']}")
        print(f"  Package Bodies:        {stats['removed_package_bodies']}")
        print(f"  Functions:             {stats['removed_functions']}")
        print(f"  Procedures:            {stats['removed_procedures']}")
        print(f"  Triggers:              {stats['removed_triggers']}")
        print()
        print("Retained by type:")
        print(f"  Packages:              {stats['retained_packages']}")
        print(f"  Package Bodies:        {stats['retained_package_bodies']}")
        print(f"  Functions:             {stats['retained_functions']}")
        print(f"  Procedures:            {stats['retained_procedures']}")
        print(f"  Triggers:              {stats['retained_triggers']}")
        print("=" * 60)
        
        if not args.dry_run:
            print(f"\nCleaned DDL written to: {args.output}")
            
            # Validate the cleaned DDL for orphans
            print("\nValidating cleaned DDL for orphaned packages...")
            has_orphans, orphan_list = check_for_orphans(args.output, args.verbose)
            
            if has_orphans:
                # Separate body-without-spec from spec-without-body
                critical_orphans = [o for o in orphan_list if o[0] == 'PACKAGE BODY']
                spec_only = [o for o in orphan_list if o[0] == 'PACKAGE']
                
                if critical_orphans:
                    print(f"\n[ERROR] Found {len(critical_orphans)} CRITICAL orphan(s) - bodies without specs:")
                    for obj_type, obj_name, reason in critical_orphans:
                        print(f"  - {obj_type} {obj_name.upper()} ({reason})")
                    print("\nThese WILL cause compilation errors!")
                
                if spec_only:
                    print(f"\n[INFO] Found {len(spec_only)} spec-only package(s) (may be intentional):")
                    for obj_type, obj_name, reason in spec_only:
                        print(f"  - {obj_type} {obj_name.upper()} ({reason})")
                    print("\nSpec-only packages are valid if they only define types/constants.")
            else:
                print("[OK] No orphaned packages found - DDL is valid")
        else:
            print("\nDRY-RUN complete - no output file created")
        
    except FileNotFoundError as e:
        print(f"\n[ERROR] File not found: {e}")
        return 1
    except json.JSONDecodeError as e:
        print(f"\n[ERROR] Invalid JSON: {e}")
        return 1
    except Exception as e:
        print(f"\n[ERROR] {e}")
        return 1
    
    return 0


if __name__ == '__main__':
    exit(main())
