#!/usr/bin/env nu

# Detect Breaking Changes Script
# Detects API changes and breaking changes between Nushell versions
#
# Usage:
#   detect_breaking_changes.nu 0.107.1 0.108.0    # Compare two versions
#   detect_breaking_changes.nu --scan-plugins      # Scan plugins for usage of breaking APIs
#   detect_breaking_changes.nu --export            # Export breaking changes report

use lib/common_lib.nu *

# Known breaking changes database
const BREAKING_CHANGES = {
    "0.108.0": [
        {
            type: "command_rename"
            old_name: "into value"
            new_name: "detect type"
            description: "Command renamed and behavior changed - doesn't operate on cells anymore"
            impact: "high"
            migration: "Replace 'into value' with 'detect type' and review usage for cell operations"
        },
        {
            type: "behavior_change"
            component: "stream_error_handling"
            description: "Collecting a stream that contains errors now raises an error itself"
            impact: "medium"
            migration: "Add explicit error handling when collecting streams that might contain errors"
        },
        {
            type: "feature_addition"
            feature: "mcp"
            description: "MCP (Model Context Protocol) feature added as optional feature"
            impact: "none"
            migration: "Enable with: cargo build --features mcp"
        }
    ],
    "0.109.0": [
        {
            type: "command_behavior_change"
            command: "split column"
            description: "Changed from 1-based to 0-based indexing for default column names"
            impact: "medium"
            old_behavior: "column1, column2, column3"
            new_behavior: "column0, column1, column2"
            migration: "Update all references to columnN to use 0-based indexing. Find affected code with: grep -r \"split column\" . --include=\"*.nu\" | grep \"column[1-9]\""
            example: {
                old: "'a b c' | split column ' ' | get column1"
                new: "'a b c' | split column ' ' | get column0"
            }
        },
        {
            type: "error_message_improvement"
            command: "http"
            description: "Dynamic HTTP method construction now produces specific error messages"
            impact: "low"
            old_behavior: "let method = 'get'; http $method url (unclear error)"
            new_behavior: "let method = 'get'; http $method url (error: 'Invalid command construction - Prefer to use `http get` directly')"
            migration: "Use direct HTTP command calls instead of dynamic construction. Find affected code with: grep -r 'http \\$' . --include=\"*.nu\""
            example: {
                old: "let method = \"get\"; http $method example.com"
                new: "http get example.com"
            }
        },
        {
            type: "feature_improvement"
            feature: "plugin_sqlite"
            description: "nu-protocol/sqlite feature no longer required for plugin compilation"
            impact: "none"
            migration: "Remove sqlite feature from plugin Cargo.toml files if not needed"
        }
    ]
}

# Main entry point
def main [
    from_version?: string         # Source version (e.g., "0.107.1")
    to_version?: string           # Target version (e.g., "0.108.0")
    --scan-plugins                # Scan plugins for breaking API usage
    --export                      # Export breaking changes report
] {
    log_info "Nushell Breaking Changes Detector"

    # If versions provided, show breaking changes between them
    if ($from_version | is-not-empty) and ($to_version | is-not-empty) {
        show_breaking_changes $from_version $to_version
    } else if $scan_plugins {
        scan_plugins_for_breaking_changes
    } else {
        # Show all known breaking changes
        show_all_breaking_changes
    }

    if $export {
        export_breaking_changes_report
    }
}

# Show breaking changes between two versions
def show_breaking_changes [
    from_version: string
    to_version: string
] {
    log_info $"Analyzing breaking changes: [$from_version] → ($to_version)"

    # Get breaking changes for target version
    let changes = get_breaking_changes_for_version $to_version

    if ($changes | is-empty) {
        log_success $"No known breaking changes between [$from_version] and ($to_version)"
        return
    }

    log_warn $"Found ($changes | length) breaking changes in version ($to_version)"

    # Display by impact level
    display_breaking_changes_by_impact $changes
}

# Get breaking changes for a specific version
def get_breaking_changes_for_version [
    version: string
]: nothing -> list<record> {
    $BREAKING_CHANGES | get -i $version | default []
}

# Display breaking changes organized by impact
def display_breaking_changes_by_impact [
    changes: list<record>
] {
    # Group by impact
    let high_impact = $changes | where impact == "high"
    let medium_impact = $changes | where impact == "medium"
    let low_impact = $changes | where impact == "low" or impact == "none"

    if ($high_impact | length) > 0 {
        log_error "\n=== HIGH IMPACT CHANGES ==="
        display_change_list $high_impact
    }

    if ($medium_impact | length) > 0 {
        log_warn "\n=== MEDIUM IMPACT CHANGES ==="
        display_change_list $medium_impact
    }

    if ($low_impact | length) > 0 {
        log_info "\n=== LOW/NO IMPACT CHANGES ==="
        display_change_list $low_impact
    }
}

# Display list of changes
def display_change_list [
    changes: list<record>
] {
    $changes | each {|change|
        let type_badge = match $change.type {
            "command_rename" => "🔄 RENAME",
            "behavior_change" => "⚠️  BEHAVIOR",
            "api_removal" => "❌ REMOVAL",
            "feature_addition" => "✨ FEATURE",
            _ => "📝 CHANGE"
        }

        print $"\n  ($type_badge)"

        if ($change | get -i old_name | is-not-empty) {
            print $"    Old: ($change.old_name) → New: ($change.new_name)"
        }

        if ($change | get -i component | is-not-empty) {
            print $"    Component: ($change.component)"
        }

        if ($change | get -i feature | is-not-empty) {
            print $"    Feature: ($change.feature)"
        }

        print $"    Description: ($change.description)"
        print $"    Migration: ($change.migration)"
    }
}

# Show all known breaking changes
def show_all_breaking_changes [] {
    log_info "All Known Breaking Changes"

    let all_versions = $BREAKING_CHANGES | columns | sort

    $all_versions | each {|version|
        let changes = $BREAKING_CHANGES | get $version
        log_info $"\n=== Version ($version) - ($changes | length) changes ==="
        display_breaking_changes_by_impact $changes
    }
}

# Scan plugins for usage of breaking APIs
def scan_plugins_for_breaking_changes [] {
    log_info "Scanning plugins for breaking API usage..."

    let plugin_dirs = get_plugin_directories

    log_info $"Scanning ($plugin_dirs | length) custom plugins"

    mut findings = []

    for plugin_dir in $plugin_dirs {
        let plugin_name = get_plugin_name $plugin_dir
        log_info $"Scanning: ($plugin_name)..."

        let plugin_findings = scan_plugin_directory $plugin_dir

        if ($plugin_findings | length) > 0 {
            $findings = ($findings | append {
                plugin: $plugin_name
                findings: $plugin_findings
            })
        }
    }

    # Display results
    if ($findings | length) > 0 {
        log_warn $"\n=== Breaking API Usage Found ==="
        $findings | each {|result|
            log_warn $"\n($result.plugin)"
            $result.findings | each {|finding|
                print $"  • ($finding.pattern) in ($finding.file):($finding.line)"
                print $"    Context: ($finding.context)"
            }
        }

        log_info "\nReview these findings and update code accordingly"
    } else {
        log_success "No breaking API usage detected!"
    }
}

# Scan a plugin directory for breaking API usage
def scan_plugin_directory [
    plugin_dir: string
]: nothing -> list<record> {
    mut findings = []

    # Get all Rust source files
    let rust_files = glob $"($plugin_dir)/src/**/*.rs"

    for file in $rust_files {
        # Check for "into value" usage (breaking in 0.108.0)
        let into_value_matches = try {
            open $file
                | lines
                | enumerate
                | where {|row| $row.item =~ "into value"}
        } catch {
            []
        }

        if ($into_value_matches | length) > 0 {
            for match in $into_value_matches {
                $findings = ($findings | append {
                    pattern: "into value (renamed to 'detect type')"
                    file: $file
                    line: ($match.index + 1)
                    context: ($match.item | str trim)
                })
            }
        }

        # Check for stream collection patterns (behavior changed in 0.108.0)
        let collect_matches = try {
            open $file
                | lines
                | enumerate
                | where {|row| $row.item =~ "collect\(\)" and $row.item =~ "stream"}
        } catch {
            []
        }

        if ($collect_matches | length) > 0 {
            for match in $collect_matches {
                $findings = ($findings | append {
                    pattern: "stream.collect() (error behavior changed)"
                    file: $file
                    line: ($match.index + 1)
                    context: ($match.item | str trim)
                })
            }
        }
    }

    $findings
}

# Export breaking changes report
def export_breaking_changes_report [] {
    let output_file = "./tmp/breaking_changes_report.json"
    ensure_dir "./tmp"

    let report = {
        generated_at: (date now | format date "%Y-%m-%d %H:%M:%S")
        breaking_changes_database: $BREAKING_CHANGES
        versions: ($BREAKING_CHANGES | columns)
    }

    $report | to json | save -f $output_file
    log_success $"Breaking changes report exported to: ($output_file)"
}

# Check if code is affected by breaking changes
def "main check-code" [
    code_snippet: string          # Code to check
] {
    log_info "Checking code for breaking API usage..."

    mut issues = []

    # Check for "into value"
    if ($code_snippet =~ "into value") {
        $issues = ($issues | append {
            pattern: "into value"
            severity: "high"
            suggestion: "Replace with 'detect type' (note: behavior changed)"
        })
    }

    # Check for direct stream collection
    if ($code_snippet =~ "\.collect\(\)") {
        $issues = ($issues | append {
            pattern: "stream.collect()"
            severity: "medium"
            suggestion: "Add explicit error handling for stream errors"
        })
    }

    # Display results
    if ($issues | length) > 0 {
        log_warn "Potential breaking API usage detected:"
        $issues | each {|issue|
            let severity_color = if $issue.severity == "high" { "red" } else { "yellow" }
            print $"  (ansi $severity_color)• ($issue.pattern)(ansi reset)"
            print $"    → ($issue.suggestion)"
        }
    } else {
        log_success "No breaking API usage detected in code snippet"
    }
}

# Generate migration guide for a specific version
def "main migration-guide" [
    version: string               # Version to generate guide for
] {
    log_info $"Generating migration guide for version ($version)"

    let changes = get_breaking_changes_for_version $version

    if ($changes | is-empty) {
        log_info $"No breaking changes documented for version ($version)"
        return
    }

    # Generate markdown migration guide
    let guide = generate_migration_markdown $version $changes

    let output_file = $"./tmp/MIGRATION_($version).md"
    ensure_dir "./tmp"

    $guide | save -f $output_file
    log_success $"Migration guide generated: ($output_file)"
}

# Generate migration guide in markdown format
def generate_migration_markdown [
    version: string
    changes: list<record>
]: nothing -> string {
    mut content = $"# Migration Guide to Nushell ($version)\n\n"
    $content = $"($content)Generated: (date now | format date '%Y-%m-%d %H:%M:%S')\n\n"

    $content = $"($content)## Breaking Changes\n\n"
    $content = $"($content)This guide covers ($changes | length) breaking changes in Nushell ($version).\n\n"

    # Group by impact
    let high_impact = $changes | where impact == "high"
    let medium_impact = $changes | where impact == "medium"
    let low_impact = $changes | where impact == "low" or impact == "none"

    if ($high_impact | length) > 0 {
        $content = $"($content)### ⚠️ High Impact Changes\n\n"
        for change in $high_impact {
            $content = ($content + (format_change_markdown $change) + "\n")
        }
    }

    if ($medium_impact | length) > 0 {
        $content = $"($content)### 📝 Medium Impact Changes\n\n"
        for change in $medium_impact {
            $content = ($content + (format_change_markdown $change) + "\n")
        }
    }

    if ($low_impact | length) > 0 {
        $content = $"($content)### ✨ Low/No Impact Changes\n\n"
        for change in $low_impact {
            $content = ($content + (format_change_markdown $change) + "\n")
        }
    }

    $content
}

# Format single change as markdown
def format_change_markdown [
    change: record
]: nothing -> string {
    mut content = $"#### ($change.type | str title-case)\n\n"

    if ($change | get -i old_name | is-not-empty) {
        $content = $"($content)**Old**: `($change.old_name)` → **New**: `($change.new_name)`\n\n"
    }

    $content = $"($content)**Description**: ($change.description)\n\n"
    $content = $"($content)**Migration**: ($change.migration)\n\n"

    $content
}

# Add a new breaking change to the database
def "main add" [
    version: string               # Version this applies to
    type: string                  # Type of change
    description: string           # Description
    impact: string                # Impact level (high/medium/low/none)
    migration: string             # Migration instructions
    --old-name: string            # Old name (for renames)
    --new-name: string            # New name (for renames)
] {
    log_info "This command would add a new breaking change to the database"
    log_warn "Note: The database is currently hardcoded in the script"
    log_info "For production use, consider storing in external TOML/JSON file"

    let new_change = {
        type: $type
        description: $description
        impact: $impact
        migration: $migration
    }

    # Add optional fields
    let new_change = if ($old_name | is-not-empty) {
        $new_change | merge {old_name: $old_name}
    } else {
        $new_change
    }

    let new_change = if ($new_name | is-not-empty) {
        $new_change | merge {new_name: $new_name}
    } else {
        $new_change
    }

    log_info "New change to add:"
    print ($new_change | to json)
}
