<%
'=======================================================================================================================
' MIGRATION SYSTEM
'=======================================================================================================================
' Provides database migration capabilities for version-controlled schema changes.
'
' Features:
' - Sequential migration versioning (timestamp-based)
' - Up/Down migration support
' - Migration tracking in schema_migrations table
' - Transaction support for atomic migrations
' - Migration status checking
'
' Usage:
'   Set migrator = Migrator()
'   migrator.ApplyMigration "20260109120000_create_users_table.asp"
'   migrator.RollbackMigration "20260109120000_create_users_table.asp"
'   pending = migrator.GetPendingMigrations()
'   applied = migrator.GetAppliedMigrations()
'

Class Migrator_Class
    Private m_db
    Private m_migrations_path
    Private m_schema_table

    '-------------------------------------------------------------------------------------------------------------------
    Private Sub Class_Initialize()
        Set m_db = DAL()
        m_schema_table = "schema_migrations"
        m_migrations_path = Server.MapPath("../db/migrations/")
    End Sub

    '-------------------------------------------------------------------------------------------------------------------
    ' Ensure the schema_migrations table exists
    '-------------------------------------------------------------------------------------------------------------------
    Public Sub EnsureSchemaMigrationsTable()
        On Error Resume Next

        ' Try to query the table - if it doesn't exist, create it
        Dim rs
        Set rs = m_db.Query("SELECT TOP 1 version FROM " & m_schema_table, empty)

        If Err.Number <> 0 Then
            ' Table doesn't exist, create it
            Err.Clear
            On Error GoTo 0

            Dim createSQL
            createSQL = "CREATE TABLE " & m_schema_table & " (" & _
                       "version VARCHAR(14) PRIMARY KEY, " & _
                       "applied_at DATETIME NOT NULL)"

            m_db.Execute createSQL, empty
        Else
            If Not rs Is Nothing Then
                rs.Close
                Set rs = Nothing
            End If
        End If

        On Error GoTo 0
    End Sub

    '-------------------------------------------------------------------------------------------------------------------
    ' Get all applied migration versions
    '-------------------------------------------------------------------------------------------------------------------
    Public Function GetAppliedMigrations()
        EnsureSchemaMigrationsTable

        Dim rs, versions, version
        Set versions = Server.CreateObject("Scripting.Dictionary")

        Set rs = m_db.Query("SELECT version FROM " & m_schema_table & " ORDER BY version", empty)

        Do While Not rs.EOF
            version = Trim(rs("version"))
            versions.Add version, True
            rs.MoveNext
        Loop

        rs.Close
        Set rs = Nothing

        Set GetAppliedMigrations = versions
    End Function

    '-------------------------------------------------------------------------------------------------------------------
    ' Get all available migration files
    '-------------------------------------------------------------------------------------------------------------------
    Public Function GetAvailableMigrations()
        Dim fso, folder, files, file
        Set fso = Server.CreateObject("Scripting.FileSystemObject")

        If Not fso.FolderExists(m_migrations_path) Then
            Set GetAvailableMigrations = Server.CreateObject("Scripting.Dictionary")
            Exit Function
        End If

        Set folder = fso.GetFolder(m_migrations_path)
        Set files = folder.Files

        Dim migrations
        Set migrations = Server.CreateObject("Scripting.Dictionary")

        For Each file In files
            If LCase(fso.GetExtensionName(file.Name)) = "asp" Then
                Dim version
                version = GetVersionFromFilename(file.Name)
                If version <> "" Then
                    migrations.Add version, file.Name
                End If
            End If
        Next

        Set GetAvailableMigrations = migrations
    End Function

    '-------------------------------------------------------------------------------------------------------------------
    ' Get pending migrations (available but not applied)
    '-------------------------------------------------------------------------------------------------------------------
    Public Function GetPendingMigrations()
        Dim applied, available, pending, version
        Set applied = GetAppliedMigrations()
        Set available = GetAvailableMigrations()
        Set pending = Server.CreateObject("Scripting.Dictionary")

        For Each version In available.Keys
            If Not applied.Exists(version) Then
                pending.Add version, available(version)
            End If
        Next

        Set GetPendingMigrations = pending
    End Function

    '-------------------------------------------------------------------------------------------------------------------
    ' Extract version from migration filename
    ' Expected format: YYYYMMDDHHMMSS_description.asp
    '-------------------------------------------------------------------------------------------------------------------
    Private Function GetVersionFromFilename(filename)
        Dim parts
        parts = Split(filename, "_")

        If UBound(parts) >= 0 Then
            Dim version
            version = parts(0)
            ' Validate it's a 14-digit timestamp
            If Len(version) = 14 And IsNumeric(version) Then
                GetVersionFromFilename = version
                Exit Function
            End If
        End If

        GetVersionFromFilename = ""
    End Function

    '-------------------------------------------------------------------------------------------------------------------
    ' Apply a migration (run the Up method)
    '-------------------------------------------------------------------------------------------------------------------
    Public Sub ApplyMigration(filename)
        Dim version
        version = GetVersionFromFilename(filename)

        If version = "" Then
            Err.Raise vbObjectError + 1, "Migrator", "Invalid migration filename format: " & filename
        End If

        ' Check if already applied
        Dim applied
        Set applied = GetAppliedMigrations()
        If applied.Exists(version) Then
            Response.Write "Migration " & version & " already applied. Skipping." & vbCrLf
            Exit Sub
        End If

        Response.Write "Applying migration: " & filename & "..." & vbCrLf

        ' Begin transaction
        m_db.BeginTransaction

        On Error Resume Next

        ' Execute the migration file
        ExecuteMigrationFile filename, "Up"

        If Err.Number <> 0 Then
            Dim errMsg
            errMsg = "Migration failed: " & Err.Description
            m_db.RollbackTransaction
            Err.Raise vbObjectError + 2, "Migrator", errMsg
        End If

        ' Record the migration
        m_db.Execute "INSERT INTO " & m_schema_table & " (version, applied_at) VALUES (?, ?)", _
                     Array(version, Now())

        If Err.Number <> 0 Then
            Dim recordErr
            recordErr = "Failed to record migration: " & Err.Description
            m_db.RollbackTransaction
            Err.Raise vbObjectError + 3, "Migrator", recordErr
        End If

        ' Commit transaction
        m_db.CommitTransaction

        On Error GoTo 0

        Response.Write "Migration " & version & " applied successfully." & vbCrLf
    End Sub

    '-------------------------------------------------------------------------------------------------------------------
    ' Rollback a migration (run the Down method)
    '-------------------------------------------------------------------------------------------------------------------
    Public Sub RollbackMigration(filename)
        Dim version
        version = GetVersionFromFilename(filename)

        If version = "" Then
            Err.Raise vbObjectError + 1, "Migrator", "Invalid migration filename format: " & filename
        End If

        ' Check if applied
        Dim applied
        Set applied = GetAppliedMigrations()
        If Not applied.Exists(version) Then
            Response.Write "Migration " & version & " not applied. Skipping." & vbCrLf
            Exit Sub
        End If

        Response.Write "Rolling back migration: " & filename & "..." & vbCrLf

        ' Begin transaction
        m_db.BeginTransaction

        On Error Resume Next

        ' Execute the migration file
        ExecuteMigrationFile filename, "Down"

        If Err.Number <> 0 Then
            Dim errMsg
            errMsg = "Rollback failed: " & Err.Description
            m_db.RollbackTransaction
            Err.Raise vbObjectError + 4, "Migrator", errMsg
        End If

        ' Remove the migration record
        m_db.Execute "DELETE FROM " & m_schema_table & " WHERE version = ?", version

        If Err.Number <> 0 Then
            Dim recordErr
            recordErr = "Failed to remove migration record: " & Err.Description
            m_db.RollbackTransaction
            Err.Raise vbObjectError + 5, "Migrator", recordErr
        End If

        ' Commit transaction
        m_db.CommitTransaction

        On Error GoTo 0

        Response.Write "Migration " & version & " rolled back successfully." & vbCrLf
    End Sub

    '-------------------------------------------------------------------------------------------------------------------
    ' Execute a migration file's Up or Down method
    '-------------------------------------------------------------------------------------------------------------------
    Private Sub ExecuteMigrationFile(filename, direction)
        Dim migrationPath
        migrationPath = m_migrations_path & filename

        Dim fso
        Set fso = Server.CreateObject("Scripting.FileSystemObject")

        If Not fso.FileExists(migrationPath) Then
            Err.Raise vbObjectError + 6, "Migrator", "Migration file not found: " & migrationPath
        End If

        ' Create a migration context that the file can use
        Dim migration
        Set migration = New MigrationContext_Class
        Set migration.DB = m_db

        ' Include and execute the migration file
        Server.Execute(migrationPath)

        ' Call the appropriate method (Up or Down)
        If direction = "Up" Then
            Execute "Call Migration_Up(migration)"
        ElseIf direction = "Down" Then
            Execute "Call Migration_Down(migration)"
        End If
    End Sub

    '-------------------------------------------------------------------------------------------------------------------
    ' Apply all pending migrations
    '-------------------------------------------------------------------------------------------------------------------
    Public Sub ApplyAllPending()
        Dim pending, version, versions()
        Set pending = GetPendingMigrations()

        If pending.Count = 0 Then
            Response.Write "No pending migrations." & vbCrLf
            Exit Sub
        End If

        ' Sort versions
        ReDim versions(pending.Count - 1)
        Dim i : i = 0
        For Each version In pending.Keys
            versions(i) = version
            i = i + 1
        Next

        ' Simple bubble sort for versions
        Dim j, temp
        For i = 0 To UBound(versions) - 1
            For j = i + 1 To UBound(versions)
                If CLng(versions(i)) > CLng(versions(j)) Then
                    temp = versions(i)
                    versions(i) = versions(j)
                    versions(j) = temp
                End If
            Next
        Next

        ' Apply in order
        For i = 0 To UBound(versions)
            ApplyMigration pending(versions(i))
        Next
    End Sub

    '-------------------------------------------------------------------------------------------------------------------
    ' Rollback the last applied migration
    '-------------------------------------------------------------------------------------------------------------------
    Public Sub RollbackLast()
        Dim applied, version, lastVersion
        Set applied = GetAppliedMigrations()

        If applied.Count = 0 Then
            Response.Write "No migrations to rollback." & vbCrLf
            Exit Sub
        End If

        ' Find the last version
        lastVersion = ""
        For Each version In applied.Keys
            If lastVersion = "" Or CLng(version) > CLng(lastVersion) Then
                lastVersion = version
            End If
        Next

        ' Find the filename
        Dim available
        Set available = GetAvailableMigrations()

        If available.Exists(lastVersion) Then
            RollbackMigration available(lastVersion)
        Else
            Err.Raise vbObjectError + 7, "Migrator", "Migration file not found for version: " & lastVersion
        End If
    End Sub

End Class

'=======================================================================================================================
' MIGRATION CONTEXT
'=======================================================================================================================
' Provides helper methods for use within migration files
'
Class MigrationContext_Class
    Public DB  ' Reference to DAL

    '-------------------------------------------------------------------------------------------------------------------
    ' Execute raw SQL
    '-------------------------------------------------------------------------------------------------------------------
    Public Sub ExecuteSQL(sql)
        DB.Execute sql, empty
    End Sub

    '-------------------------------------------------------------------------------------------------------------------
    ' Create a table
    '-------------------------------------------------------------------------------------------------------------------
    Public Sub CreateTable(tableName, columns)
        Dim sql
        sql = "CREATE TABLE " & tableName & " (" & columns & ")"
        ExecuteSQL sql
    End Sub

    '-------------------------------------------------------------------------------------------------------------------
    ' Drop a table
    '-------------------------------------------------------------------------------------------------------------------
    Public Sub DropTable(tableName)
        ExecuteSQL "DROP TABLE " & tableName
    End Sub

    '-------------------------------------------------------------------------------------------------------------------
    ' Add a column to a table
    '-------------------------------------------------------------------------------------------------------------------
    Public Sub AddColumn(tableName, columnName, columnType)
        ExecuteSQL "ALTER TABLE " & tableName & " ADD COLUMN " & columnName & " " & columnType
    End Sub

    '-------------------------------------------------------------------------------------------------------------------
    ' Drop a column from a table
    '-------------------------------------------------------------------------------------------------------------------
    Public Sub DropColumn(tableName, columnName)
        ExecuteSQL "ALTER TABLE " & tableName & " DROP COLUMN " & columnName
    End Sub

    '-------------------------------------------------------------------------------------------------------------------
    ' Create an index
    '-------------------------------------------------------------------------------------------------------------------
    Public Sub CreateIndex(indexName, tableName, columns)
        ExecuteSQL "CREATE INDEX " & indexName & " ON " & tableName & " (" & columns & ")"
    End Sub

    '-------------------------------------------------------------------------------------------------------------------
    ' Drop an index
    '-------------------------------------------------------------------------------------------------------------------
    Public Sub DropIndex(indexName, tableName)
        ExecuteSQL "DROP INDEX " & indexName & " ON " & tableName
    End Sub
End Class

'=======================================================================================================================
' SINGLETON
'=======================================================================================================================
Dim Migrator__Singleton : Set Migrator__Singleton = Nothing

Function Migrator()
    If Migrator__Singleton Is Nothing Then
        Set Migrator__Singleton = New Migrator_Class
    End If
    Set Migrator = Migrator__Singleton
End Function
%>
