Imports System
Imports System.Collections.Generic
Imports System.Diagnostics
Imports System.Drawing
Imports System.IO
Imports System.Linq
Imports System.Text
Imports System.Text.Json
Imports System.Threading.Tasks
Imports System.Windows.Forms

Friend Class MainForm
    Inherits Form

    Private ReadOnly _projects As New List(Of ProjectItem)()
    Private ReadOnly _filteredProjects As New List(Of ProjectItem)()
    Private ReadOnly _suiteRoot As String
    Private ReadOnly _catalogPath As String

    Private ReadOnly _txtSearch As New TextBox()
    Private ReadOnly _lstProjects As New ListBox()
    Private ReadOnly _lblTitle As New Label()
    Private ReadOnly _lblCategory As New Label()
    Private ReadOnly _lblStack As New Label()
    Private ReadOnly _txtSummary As New TextBox()
    Private ReadOnly _txtCommand As New TextBox()
    Private ReadOnly _txtOutput As New RichTextBox()
    Private ReadOnly _btnRunDemo As New Button()
    Private ReadOnly _btnOpenApp As New Button()
    Private ReadOnly _btnOpenFolder As New Button()
    Private ReadOnly _btnCopyCommand As New Button()
    Private ReadOnly _btnRefresh As New Button()
    Private ReadOnly _lblStatus As New Label()
    Private ReadOnly _bodySplit As New SplitContainer()

    Public Sub New()
        Text = "Portfolio Suite Launcher VB.NET"
        StartPosition = FormStartPosition.CenterScreen
        MinimumSize = New Size(1440, 860)
        Size = New Size(1680, 980)
        Font = New Font("Segoe UI", 10.0F)
        BackColor = Color.FromArgb(241, 245, 249)
        DoubleBuffered = True

        _suiteRoot = FindSuiteRoot(AppContext.BaseDirectory)
        _catalogPath = Path.Combine(_suiteRoot, "suite-catalog.json")

        BuildUi()
        LoadCatalog()
    End Sub

    Protected Overrides Sub OnShown(e As EventArgs)
        MyBase.OnShown(e)
        BeginInvoke(New MethodInvoker(AddressOf AdjustSplitSafe))
    End Sub

    Protected Overrides Sub OnResize(e As EventArgs)
        MyBase.OnResize(e)
        AdjustSplitSafe()
    End Sub

    Private Sub BuildUi()
        SuspendLayout()
        Controls.Clear()

        Dim rootLayout As New TableLayoutPanel With {
            .Dock = DockStyle.Fill,
            .ColumnCount = 1,
            .RowCount = 2,
            .BackColor = Color.FromArgb(241, 245, 249),
            .Padding = New Padding(12)
        }
        rootLayout.RowStyles.Add(New RowStyle(SizeType.AutoSize))
        rootLayout.RowStyles.Add(New RowStyle(SizeType.Percent, 100.0F))
        Controls.Add(rootLayout)

Dim header As New Panel With {
    .Dock = DockStyle.Top,
    .BackColor = Color.FromArgb(15, 23, 42),
    .Margin = New Padding(0, 0, 0, 12),
    .Padding = New Padding(24, 18, 24, 16),
    .Height = 96
}

Dim headerLayout As New TableLayoutPanel With {
    .Dock = DockStyle.Fill,
    .ColumnCount = 1,
    .RowCount = 2,
    .BackColor = Color.Transparent,
    .Margin = New Padding(0),
    .Padding = New Padding(0)
}
headerLayout.RowStyles.Add(New RowStyle(SizeType.Absolute, 42.0F))
headerLayout.RowStyles.Add(New RowStyle(SizeType.Absolute, 24.0F))
header.Controls.Add(headerLayout)

Dim title As New Label With {
    .Text = "Portfolio Suite Launcher",
    .ForeColor = Color.White,
    .Font = New Font("Segoe UI Semibold", 22.0F, FontStyle.Bold),
    .Dock = DockStyle.Fill,
    .TextAlign = ContentAlignment.MiddleLeft,
    .AutoSize = False,
    .AutoEllipsis = True,
    .Margin = New Padding(0, 0, 0, 4)
}

Dim subtitle As New Label With {
    .Text = "Pilih proyek, jalankan demo, buka aplikasi, dan lihat hasil proses langsung dari launcher yang lebih rapi.",
    .ForeColor = Color.FromArgb(203, 213, 225),
    .Font = New Font("Segoe UI", 10.5F, FontStyle.Regular),
    .Dock = DockStyle.Fill,
    .TextAlign = ContentAlignment.MiddleLeft,
    .AutoSize = False,
    .AutoEllipsis = True,
    .Margin = New Padding(0)
}

headerLayout.Controls.Add(title, 0, 0)
headerLayout.Controls.Add(subtitle, 0, 1)
rootLayout.Controls.Add(header, 0, 0)

        _bodySplit.Dock = DockStyle.Fill
        _bodySplit.BackColor = Color.FromArgb(241, 245, 249)
        _bodySplit.BorderStyle = BorderStyle.None
        _bodySplit.FixedPanel = FixedPanel.None
        _bodySplit.Panel1MinSize = 0
        _bodySplit.Panel2MinSize = 0
        _bodySplit.SplitterWidth = 8
        _bodySplit.Margin = New Padding(0)
        _bodySplit.Panel1.Padding = New Padding(0, 0, 10, 0)
        _bodySplit.Panel2.Padding = New Padding(10, 0, 0, 0)
        AddHandler _bodySplit.SizeChanged, AddressOf OnBodySplitSizeChanged
        rootLayout.Controls.Add(_bodySplit, 0, 1)

        Dim leftCard As New Panel With {
            .Dock = DockStyle.Fill,
            .BackColor = Color.White,
            .Padding = New Padding(18)
        }
        _bodySplit.Panel1.Controls.Add(leftCard)

        Dim leftLayout As New TableLayoutPanel With {
            .Dock = DockStyle.Fill,
            .ColumnCount = 1,
            .RowCount = 4
        }
        leftLayout.RowStyles.Add(New RowStyle(SizeType.AutoSize))
        leftLayout.RowStyles.Add(New RowStyle(SizeType.AutoSize))
        leftLayout.RowStyles.Add(New RowStyle(SizeType.AutoSize))
        leftLayout.RowStyles.Add(New RowStyle(SizeType.Percent, 100.0F))
        leftCard.Controls.Add(leftLayout)

        Dim leftTitle As New Label With {
            .Text = "Daftar Proyek",
            .Dock = DockStyle.Top,
            .Font = New Font("Segoe UI Semibold", 18.0F, FontStyle.Bold),
            .Height = 36
        }
        leftLayout.Controls.Add(leftTitle, 0, 0)

        Dim leftSub As New Label With {
            .Text = "Cari cepat berdasarkan nama, stack, atau ringkasan proyek.",
            .Dock = DockStyle.Top,
            .ForeColor = Color.FromArgb(71, 85, 105),
            .Height = 24
        }
        leftLayout.Controls.Add(leftSub, 0, 1)

        _txtSearch.Dock = DockStyle.Top
        _txtSearch.Margin = New Padding(0, 8, 0, 14)
        _txtSearch.BackColor = Color.FromArgb(248, 250, 252)
        _txtSearch.BorderStyle = BorderStyle.FixedSingle
        _txtSearch.PlaceholderText = "Cari proyek, mis. helpdesk / python / dashboard"
        AddHandler _txtSearch.TextChanged, AddressOf OnSearchChanged
        leftLayout.Controls.Add(_txtSearch, 0, 2)

        _lstProjects.Dock = DockStyle.Fill
        _lstProjects.IntegralHeight = False
        _lstProjects.BorderStyle = BorderStyle.FixedSingle
        _lstProjects.BackColor = Color.White
        _lstProjects.Font = New Font("Segoe UI", 11.0F)
        AddHandler _lstProjects.SelectedIndexChanged, AddressOf OnProjectChanged
        leftLayout.Controls.Add(_lstProjects, 0, 3)

        Dim rightCard As New Panel With {
            .Dock = DockStyle.Fill,
            .BackColor = Color.White,
            .Padding = New Padding(18)
        }
        _bodySplit.Panel2.Controls.Add(rightCard)

        Dim rightLayout As New TableLayoutPanel With {
            .Dock = DockStyle.Fill,
            .ColumnCount = 1,
            .RowCount = 6
        }
        rightLayout.RowStyles.Add(New RowStyle(SizeType.AutoSize))
        rightLayout.RowStyles.Add(New RowStyle(SizeType.AutoSize))
        rightLayout.RowStyles.Add(New RowStyle(SizeType.Absolute, 146.0F))
        rightLayout.RowStyles.Add(New RowStyle(SizeType.Absolute, 70.0F))
        rightLayout.RowStyles.Add(New RowStyle(SizeType.AutoSize))
        rightLayout.RowStyles.Add(New RowStyle(SizeType.Percent, 100.0F))
        rightCard.Controls.Add(rightLayout)

        _lblTitle.AutoSize = True
        _lblTitle.Font = New Font("Segoe UI Semibold", 20.0F, FontStyle.Bold)
        _lblTitle.Text = "Pilih proyek"
        rightLayout.Controls.Add(_lblTitle, 0, 0)

        Dim badgeWrap As New FlowLayoutPanel With {
            .Dock = DockStyle.Top,
            .AutoSize = True,
            .WrapContents = True,
            .FlowDirection = FlowDirection.LeftToRight,
            .Margin = New Padding(0, 8, 0, 12)
        }

        _lblCategory.AutoSize = True
        _lblCategory.BackColor = Color.FromArgb(219, 234, 254)
        _lblCategory.ForeColor = Color.FromArgb(30, 64, 175)
        _lblCategory.Padding = New Padding(10, 5, 10, 5)
        _lblCategory.Font = New Font("Segoe UI", 9.0F, FontStyle.Bold)
        _lblCategory.Text = "Kategori"

        _lblStack.AutoSize = True
        _lblStack.BackColor = Color.FromArgb(226, 232, 240)
        _lblStack.ForeColor = Color.FromArgb(15, 23, 42)
        _lblStack.Padding = New Padding(10, 5, 10, 5)
        _lblStack.Font = New Font("Segoe UI", 9.0F, FontStyle.Bold)
        _lblStack.Text = "Stack"

        badgeWrap.Controls.Add(_lblCategory)
        badgeWrap.Controls.Add(_lblStack)
        rightLayout.Controls.Add(badgeWrap, 0, 1)

        _txtSummary.Multiline = True
        _txtSummary.ReadOnly = True
        _txtSummary.BackColor = Color.FromArgb(248, 250, 252)
        _txtSummary.BorderStyle = BorderStyle.FixedSingle
        _txtSummary.Dock = DockStyle.Fill
        _txtSummary.ScrollBars = ScrollBars.Vertical
        _txtSummary.Text = "Pilih proyek di sebelah kiri."
        rightLayout.Controls.Add(_txtSummary, 0, 2)

        _txtCommand.ReadOnly = True
        _txtCommand.Multiline = True
        _txtCommand.Dock = DockStyle.Fill
        _txtCommand.BackColor = Color.FromArgb(248, 250, 252)
        _txtCommand.BorderStyle = BorderStyle.FixedSingle
        _txtCommand.Text = "Perintah utama: -"
        rightLayout.Controls.Add(_txtCommand, 0, 3)

        Dim buttonsGrid As New TableLayoutPanel With {
            .Dock = DockStyle.Top,
            .ColumnCount = 2,
            .RowCount = 3,
            .AutoSize = True,
            .Margin = New Padding(0, 2, 0, 12)
        }
        buttonsGrid.ColumnStyles.Add(New ColumnStyle(SizeType.Percent, 50.0F))
        buttonsGrid.ColumnStyles.Add(New ColumnStyle(SizeType.Percent, 50.0F))
        buttonsGrid.RowStyles.Add(New RowStyle(SizeType.AutoSize))
        buttonsGrid.RowStyles.Add(New RowStyle(SizeType.AutoSize))
        buttonsGrid.RowStyles.Add(New RowStyle(SizeType.AutoSize))

        SetupButton(_btnRunDemo, "Jalankan Demo", AddressOf OnRunDemo, Color.FromArgb(37, 99, 235))
        SetupButton(_btnOpenApp, "Buka Aplikasi", AddressOf OnOpenApp, Color.FromArgb(14, 116, 144))
        SetupButton(_btnOpenFolder, "Buka Folder", AddressOf OnOpenFolder, Color.FromArgb(71, 85, 105))
        SetupButton(_btnCopyCommand, "Salin Perintah", AddressOf OnCopyCommand, Color.FromArgb(30, 41, 59))
        SetupButton(_btnRefresh, "Muat Ulang", AddressOf OnRefreshCatalog, Color.FromArgb(22, 163, 74))

        buttonsGrid.Controls.Add(_btnRunDemo, 0, 0)
        buttonsGrid.Controls.Add(_btnOpenApp, 1, 0)
        buttonsGrid.Controls.Add(_btnOpenFolder, 0, 1)
        buttonsGrid.Controls.Add(_btnCopyCommand, 1, 1)
        buttonsGrid.Controls.Add(_btnRefresh, 0, 2)
        buttonsGrid.SetColumnSpan(_btnRefresh, 2)
        rightLayout.Controls.Add(buttonsGrid, 0, 4)

        Dim outputCard As New TableLayoutPanel With {
            .Dock = DockStyle.Fill,
            .ColumnCount = 1,
            .RowCount = 3
        }
        outputCard.RowStyles.Add(New RowStyle(SizeType.AutoSize))
        outputCard.RowStyles.Add(New RowStyle(SizeType.AutoSize))
        outputCard.RowStyles.Add(New RowStyle(SizeType.Percent, 100.0F))

        Dim outputTitle As New Label With {
            .Text = "Output Proses",
            .Dock = DockStyle.Top,
            .Font = New Font("Segoe UI Semibold", 12.0F, FontStyle.Bold),
            .Height = 28
        }

        _lblStatus.AutoSize = True
        _lblStatus.Text = "Status: Siap"
        _lblStatus.Font = New Font("Segoe UI", 10.0F, FontStyle.Bold)
        _lblStatus.ForeColor = Color.FromArgb(22, 163, 74)
        _lblStatus.Margin = New Padding(0, 0, 0, 8)

        _txtOutput.Dock = DockStyle.Fill
        _txtOutput.ReadOnly = True
        _txtOutput.Font = New Font("Consolas", 9.5F)
        _txtOutput.BackColor = Color.FromArgb(248, 250, 252)
        _txtOutput.BorderStyle = BorderStyle.FixedSingle
        _txtOutput.Text = "Output demo akan tampil di sini..."

        outputCard.Controls.Add(outputTitle, 0, 0)
        outputCard.Controls.Add(_lblStatus, 0, 1)
        outputCard.Controls.Add(_txtOutput, 0, 2)
        rightLayout.Controls.Add(outputCard, 0, 5)

        ResumeLayout()
    End Sub

    Private Sub AdjustSplitSafe()
        Try
            AdjustSplit()
        Catch
            ' Abaikan error layout awal agar startup tetap aman.
        End Try
    End Sub

    Private Sub OnBodySplitSizeChanged(sender As Object, e As EventArgs)
        AdjustSplitSafe()
    End Sub

    Private Sub AdjustSplit()
        If _bodySplit Is Nothing OrElse _bodySplit.IsDisposed OrElse Not _bodySplit.IsHandleCreated Then
            Return
        End If

        Dim usableWidth As Integer = Math.Max(0, _bodySplit.ClientSize.Width - _bodySplit.SplitterWidth)
        If usableWidth < 320 Then
            Return
        End If

        Dim minLeft As Integer = 360
        Dim minRight As Integer = 560

        If usableWidth < (minLeft + minRight) Then
            minLeft = Math.Max(200, CInt(Math.Truncate(usableWidth * 0.38R)))
            minRight = Math.Max(200, usableWidth - minLeft)
        End If

        Dim maxLeft As Integer = usableWidth - minRight
        If maxLeft < 0 Then
            maxLeft = usableWidth
        End If

        Dim target As Integer = CInt(Math.Truncate(usableWidth * 0.56R))
        If target < minLeft Then target = minLeft
        If target > maxLeft Then target = maxLeft
        If target < 0 Then target = 0
        If target > usableWidth Then target = usableWidth

        If _bodySplit.SplitterDistance <> target Then
            _bodySplit.SplitterDistance = target
        End If
    End Sub

    Private Sub SetupButton(button As Button, buttonText As String, handler As EventHandler, buttonColor As Color)
        button.Text = buttonText
        button.AutoSize = False
        button.Height = 46
        button.Dock = DockStyle.Fill
        button.Margin = New Padding(0, 0, 10, 10)
        button.BackColor = buttonColor
        button.ForeColor = Color.White
        button.FlatStyle = FlatStyle.Flat
        button.FlatAppearance.BorderSize = 0
        button.FlatAppearance.MouseOverBackColor = ControlPaint.Light(buttonColor, 0.08F)
        button.FlatAppearance.MouseDownBackColor = ControlPaint.Dark(buttonColor, 0.08F)
        AddHandler button.Click, handler
    End Sub

    Private Function FindSuiteRoot(startPath As String) As String
    Dim preferred = FindSuiteRootWithProjects(startPath)
    If Not String.IsNullOrWhiteSpace(preferred) Then
        Return preferred
    End If

    Dim fallback = FindSuiteRootWithProjects(Environment.CurrentDirectory)
    If Not String.IsNullOrWhiteSpace(fallback) Then
        Return fallback
    End If

    Dim catalogOnly = FindSuiteRootByCatalog(startPath)
    If Not String.IsNullOrWhiteSpace(catalogOnly) Then
        Return catalogOnly
    End If

    Dim currentCatalogOnly = FindSuiteRootByCatalog(Environment.CurrentDirectory)
    If Not String.IsNullOrWhiteSpace(currentCatalogOnly) Then
        Return currentCatalogOnly
    End If

    Return Environment.CurrentDirectory
End Function

Private Function FindSuiteRootWithProjects(startPath As String) As String
    If String.IsNullOrWhiteSpace(startPath) Then
        Return Nothing
    End If

    Dim dir As DirectoryInfo = New DirectoryInfo(startPath)
    Do While dir IsNot Nothing
        Dim catalog = Path.Combine(dir.FullName, "suite-catalog.json")
        Dim projectsDir = Path.Combine(dir.FullName, "projects")
        If File.Exists(catalog) AndAlso Directory.Exists(projectsDir) Then
            Return dir.FullName
        End If
        dir = dir.Parent
    Loop

    Return Nothing
End Function

Private Function FindSuiteRootByCatalog(startPath As String) As String
    If String.IsNullOrWhiteSpace(startPath) Then
        Return Nothing
    End If

    Dim dir As DirectoryInfo = New DirectoryInfo(startPath)
    Do While dir IsNot Nothing
        Dim catalog = Path.Combine(dir.FullName, "suite-catalog.json")
        If File.Exists(catalog) Then
            Return dir.FullName
        End If
        dir = dir.Parent
    Loop

    Return Nothing
End Function


    Private Sub LoadCatalog()
        _projects.Clear()

        If Not File.Exists(_catalogPath) Then
            _txtOutput.Text = "suite-catalog.json tidak ditemukan. Jalankan launcher dari folder paket utama."
            SetStatus("Status: Catalog tidak ditemukan", Color.FromArgb(220, 38, 38))
            Return
        End If

        Try
            Dim json = File.ReadAllText(_catalogPath, Encoding.UTF8)
            Dim options As New JsonSerializerOptions With {
                .PropertyNameCaseInsensitive = True
            }
            Dim items = JsonSerializer.Deserialize(Of List(Of ProjectItem))(json, options)

            If items Is Nothing OrElse items.Count = 0 Then
                _txtOutput.Text = "Catalog kosong atau gagal dibaca."
                SetStatus("Status: Catalog invalid", Color.FromArgb(220, 38, 38))
                Return
            End If

            _projects.AddRange(items)
            ApplyFilter(Nothing)
        Catch ex As Exception
            _txtOutput.Text = ex.ToString()
            SetStatus("Status: Gagal membaca catalog", Color.FromArgb(220, 38, 38))
        End Try
    End Sub

    Private Sub OnSearchChanged(sender As Object, e As EventArgs)
        Dim keepId As String = Nothing
        Dim current = CurrentProject()
        If current IsNot Nothing Then keepId = current.Id
        ApplyFilter(keepId)
    End Sub

    Private Sub ApplyFilter(preferredId As String)
        _filteredProjects.Clear()
        _lstProjects.BeginUpdate()
        _lstProjects.Items.Clear()

        Dim keyword As String = _txtSearch.Text.Trim().ToLowerInvariant()

        For Each item In _projects
            Dim haystack = (If(item.Name, "") & " " & If(item.Stack, "") & " " & If(item.Summary, "")).ToLowerInvariant()
            If String.IsNullOrWhiteSpace(keyword) OrElse haystack.Contains(keyword) Then
                _filteredProjects.Add(item)
                _lstProjects.Items.Add(item)
            End If
        Next

        _lstProjects.EndUpdate()

        If _lstProjects.Items.Count = 0 Then
            _lblTitle.Text = "Tidak ada hasil"
            _lblCategory.Text = "Filter aktif"
            _lblStack.Text = "-"
            _txtSummary.Text = "Tidak ada proyek yang cocok dengan kata kunci saat ini."
            _txtCommand.Text = "Perintah utama: -"
            _txtOutput.Text = "Ubah kata kunci pencarian atau klik 'Muat Ulang'."
            SetStatus("Status: Tidak ada hasil", Color.FromArgb(202, 138, 4))
            Return
        End If

        Dim index As Integer = 0
        If Not String.IsNullOrWhiteSpace(preferredId) Then
            For i = 0 To _filteredProjects.Count - 1
                If String.Equals(_filteredProjects(i).Id, preferredId, StringComparison.OrdinalIgnoreCase) Then
                    index = i
                    Exit For
                End If
            Next
        End If

        _lstProjects.SelectedIndex = index
    End Sub

    Private Sub OnProjectChanged(sender As Object, e As EventArgs)
        Dim item = CurrentProject()
        If item Is Nothing Then
            Return
        End If

        _lblTitle.Text = item.Name
        _lblCategory.Text = InferCategory(item)
        _lblStack.Text = item.Stack
        _txtSummary.Text = item.Summary & Environment.NewLine & Environment.NewLine &
                           "Folder: " & item.Folder
        _txtCommand.Text = "Perintah utama: " & If(String.IsNullOrWhiteSpace(item.RunHint), "-", item.RunHint)
        SetStatus("Status: Siap", Color.FromArgb(22, 163, 74))
    End Sub

    Private Function CurrentProject() As ProjectItem
        Return TryCast(_lstProjects.SelectedItem, ProjectItem)
    End Function

    Private Function InferCategory(item As ProjectItem) As String
        Dim text = (If(item.Name, "") & " " & If(item.Stack, "") & " " & If(item.Summary, "")).ToLowerInvariant()
        If text.Contains("winforms") OrElse text.Contains("tkinter") OrElse text.Contains("swing") Then
            Return "Desktop App"
        End If
        If text.Contains("dashboard") OrElse text.Contains("http") OrElse text.Contains("api") OrElse text.Contains("web") Then
            Return "Web / API"
        End If
        If text.Contains("shell") OrElse text.Contains("toolkit") Then
            Return "Toolkit"
        End If
        Return "Console / Utility"
    End Function

    Private Async Sub OnRunDemo(sender As Object, e As EventArgs)
        Dim item = CurrentProject()
        If item Is Nothing Then Return
        Await ExecuteScriptAsync(item, item.DemoScript, "Menjalankan " & item.Name & "...")
    End Sub

    Private Async Sub OnOpenApp(sender As Object, e As EventArgs)
        Dim item = CurrentProject()
        If item Is Nothing Then Return
        Await ExecuteScriptAsync(item, item.OpenScript, "Membuka aplikasi " & item.Name & "...")
    End Sub

    Private Sub OnOpenFolder(sender As Object, e As EventArgs)
        Dim item = CurrentProject()
        If item Is Nothing Then Return

        Dim projectDir = GetProjectDirectory(item)
        If Not Directory.Exists(projectDir) Then
            _txtOutput.Text = "Folder proyek tidak ditemukan: " & projectDir
            SetStatus("Status: Folder tidak ditemukan", Color.FromArgb(220, 38, 38))
            Return
        End If

        Process.Start(New ProcessStartInfo With {
            .FileName = "explorer.exe",
            .Arguments = """" & projectDir & """",
            .UseShellExecute = True
        })
        SetStatus("Status: Folder dibuka", Color.FromArgb(14, 116, 144))
    End Sub

    Private Sub OnCopyCommand(sender As Object, e As EventArgs)
        Dim item = CurrentProject()
        If item Is Nothing Then Return

        Dim cmd = If(String.IsNullOrWhiteSpace(item.RunHint), "-", item.RunHint)
        Clipboard.SetText(cmd)
        SetStatus("Status: Perintah disalin", Color.FromArgb(22, 163, 74))
    End Sub

    Private Sub OnRefreshCatalog(sender As Object, e As EventArgs)
        LoadCatalog()
        SetStatus("Status: Catalog dimuat ulang", Color.FromArgb(22, 163, 74))
    End Sub

    Private Function GetProjectDirectory(item As ProjectItem) As String
        Return Path.GetFullPath(Path.Combine(_suiteRoot, item.Folder.Replace("/", Path.DirectorySeparatorChar)))
    End Function

    Private Async Function ExecuteScriptAsync(item As ProjectItem, scriptName As String, banner As String) As Task
        Dim projectDir = GetProjectDirectory(item)
        If Not Directory.Exists(projectDir) Then
            _txtOutput.Text = "Folder proyek tidak ditemukan: " & projectDir
            SetStatus("Status: Folder tidak ditemukan", Color.FromArgb(220, 38, 38))
            Exit Function
        End If

        Dim scriptPath = Path.Combine(projectDir, scriptName)
        If Not File.Exists(scriptPath) Then
            _txtOutput.Text = "Script tidak ditemukan: " & scriptPath
            SetStatus("Status: Script tidak ditemukan", Color.FromArgb(220, 38, 38))
            Exit Function
        End If

        _btnRunDemo.Enabled = False
        _btnOpenApp.Enabled = False
        _btnRefresh.Enabled = False
        _txtOutput.Text = banner & Environment.NewLine
        SetStatus("Status: Sedang berjalan...", Color.FromArgb(202, 138, 4))

        Dim psi As New ProcessStartInfo With {
            .FileName = "powershell.exe",
            .Arguments = "-NoProfile -ExecutionPolicy Bypass -File " & """" & scriptPath & """",
            .WorkingDirectory = projectDir,
            .UseShellExecute = False,
            .RedirectStandardOutput = True,
            .RedirectStandardError = True,
            .CreateNoWindow = True,
            .StandardOutputEncoding = Encoding.UTF8,
            .StandardErrorEncoding = Encoding.UTF8
        }

        Dim process As New Process With {.StartInfo = psi, .EnableRaisingEvents = True}
        Dim outBuffer As New StringBuilder()
        Dim errBuffer As New StringBuilder()

        AddHandler process.OutputDataReceived,
            Sub(senderObj As Object, args As DataReceivedEventArgs)
                If args.Data Is Nothing Then Return
                SyncLock outBuffer
                    outBuffer.AppendLine(args.Data)
                End SyncLock
            End Sub

        AddHandler process.ErrorDataReceived,
            Sub(senderObj As Object, args As DataReceivedEventArgs)
                If args.Data Is Nothing Then Return
                SyncLock errBuffer
                    errBuffer.AppendLine("[ERR] " & args.Data)
                End SyncLock
            End Sub

        Try
            process.Start()
            process.BeginOutputReadLine()
            process.BeginErrorReadLine()
            Await Task.Run(Sub() process.WaitForExit())

            Dim finalText As String
            SyncLock outBuffer
                finalText = outBuffer.ToString()
            End SyncLock
            SyncLock errBuffer
                finalText &= errBuffer.ToString()
            End SyncLock

            If String.IsNullOrWhiteSpace(finalText) Then
                finalText = banner & Environment.NewLine & "Selesai tanpa output tambahan."
            ElseIf Not finalText.StartsWith(banner, StringComparison.Ordinal) Then
                finalText = banner & Environment.NewLine & finalText
            End If

            _txtOutput.Text = finalText.TrimEnd()

            If process.ExitCode = 0 Then
                SetStatus("Status: Selesai", Color.FromArgb(22, 163, 74))
            Else
                SetStatus("Status: Ada error (kode " & process.ExitCode.ToString() & ")", Color.FromArgb(220, 38, 38))
            End If
        Catch ex As Exception
            _txtOutput.Text = banner & Environment.NewLine & ex.ToString()
            SetStatus("Status: Gagal menjalankan script", Color.FromArgb(220, 38, 38))
        Finally
            _btnRunDemo.Enabled = True
            _btnOpenApp.Enabled = True
            _btnRefresh.Enabled = True
            process.Dispose()
        End Try
    End Function

    Private Sub SetStatus(text As String, color As Color)
        _lblStatus.Text = text
        _lblStatus.ForeColor = color
    End Sub
End Class
