Option Strict On
Option Explicit On

Imports System
Imports System.Collections.Generic
Imports System.Collections.ObjectModel
Imports System.Globalization
Imports System.IO
Imports System.Linq
Imports System.Xml.Linq

Namespace Nexamas.UI.Certification

    ''' <summary>
    ''' Severity for the final dead-path / parallel-path source tree closure audit.
    ''' </summary>
    Friend Enum MASDeadPathClosureFindingSeverity
        Info = 0
        Warning = 1
        [Error] = 2
    End Enum

    ''' <summary>
    ''' Immutable finding emitted by the dead-path closure audit.
    ''' </summary>
    Friend NotInheritable Class MASDeadPathClosureFinding
        Private Sub New(severity As MASDeadPathClosureFindingSeverity,
                        code As String,
                        subject As String,
                        message As String)
            Me.Severity = severity
            Me.Code = NormalizeText(code)
            Me.Subject = NormalizeText(subject)
            Me.Message = NormalizeText(message)
        End Sub

        Friend ReadOnly Property Severity As MASDeadPathClosureFindingSeverity
        Friend ReadOnly Property Code As String
        Friend ReadOnly Property Subject As String
        Friend ReadOnly Property Message As String

        Friend Shared Function Info(code As String,
                                    subject As String,
                                    message As String) As MASDeadPathClosureFinding
            Return New MASDeadPathClosureFinding(MASDeadPathClosureFindingSeverity.Info, code, subject, message)
        End Function

        Friend Shared Function Warning(code As String,
                                       subject As String,
                                       message As String) As MASDeadPathClosureFinding
            Return New MASDeadPathClosureFinding(MASDeadPathClosureFindingSeverity.Warning, code, subject, message)
        End Function

        Friend Shared Function [Error](code As String,
                                       subject As String,
                                       message As String) As MASDeadPathClosureFinding
            Return New MASDeadPathClosureFinding(MASDeadPathClosureFindingSeverity.Error, code, subject, message)
        End Function

        Public Overrides Function ToString() As String
            Return Severity.ToString() & " | " & Code & " | " & Subject & " | " & Message
        End Function

        Private Shared Function NormalizeText(value As String) As String
            Return If(value, String.Empty).Trim()
        End Function
    End Class

    ''' <summary>
    ''' Immutable report for project-root dead-path / parallel-path closure.
    ''' </summary>
    Friend NotInheritable Class MASDeadPathClosureReport
        Private ReadOnly _findings As ReadOnlyCollection(Of MASDeadPathClosureFinding)

        Friend Sub New(projectAssetCount As Integer,
                       compileAssetCount As Integer,
                       activeSourceFileCount As Integer,
                       unownedActiveSourceCount As Integer,
                       missingDeclaredAssetCount As Integer,
                       duplicateIncludeCount As Integer,
                       blockingGeneratedOutputCount As Integer,
                       monitoredGeneratedOutputCount As Integer,
                       findings As IEnumerable(Of MASDeadPathClosureFinding))
            Me.ProjectAssetCount = Math.Max(0, projectAssetCount)
            Me.CompileAssetCount = Math.Max(0, compileAssetCount)
            Me.ActiveSourceFileCount = Math.Max(0, activeSourceFileCount)
            Me.UnownedActiveSourceCount = Math.Max(0, unownedActiveSourceCount)
            Me.MissingDeclaredAssetCount = Math.Max(0, missingDeclaredAssetCount)
            Me.DuplicateIncludeCount = Math.Max(0, duplicateIncludeCount)
            Me.BlockingGeneratedOutputCount = Math.Max(0, blockingGeneratedOutputCount)
            Me.MonitoredGeneratedOutputCount = Math.Max(0, monitoredGeneratedOutputCount)

            Dim normalized As New List(Of MASDeadPathClosureFinding)()
            If findings IsNot Nothing Then
                For Each finding As MASDeadPathClosureFinding In findings
                    If finding IsNot Nothing Then normalized.Add(finding)
                Next
            End If
            _findings = New ReadOnlyCollection(Of MASDeadPathClosureFinding)(normalized)
        End Sub

        Friend ReadOnly Property ProjectAssetCount As Integer
        Friend ReadOnly Property CompileAssetCount As Integer
        Friend ReadOnly Property ActiveSourceFileCount As Integer
        Friend ReadOnly Property UnownedActiveSourceCount As Integer
        Friend ReadOnly Property MissingDeclaredAssetCount As Integer
        Friend ReadOnly Property DuplicateIncludeCount As Integer
        Friend ReadOnly Property BlockingGeneratedOutputCount As Integer
        Friend ReadOnly Property MonitoredGeneratedOutputCount As Integer

        Friend ReadOnly Property Findings As ReadOnlyCollection(Of MASDeadPathClosureFinding)
            Get
                Return _findings
            End Get
        End Property

        Friend ReadOnly Property ErrorCount As Integer
            Get
                Return CountBySeverity(MASDeadPathClosureFindingSeverity.Error)
            End Get
        End Property

        Friend ReadOnly Property WarningCount As Integer
            Get
                Return CountBySeverity(MASDeadPathClosureFindingSeverity.Warning)
            End Get
        End Property

        Friend ReadOnly Property IsClosed As Boolean
            Get
                Return ProjectAssetCount > 0 AndAlso
                       CompileAssetCount > 0 AndAlso
                       ActiveSourceFileCount > 0 AndAlso
                       UnownedActiveSourceCount = 0 AndAlso
                       MissingDeclaredAssetCount = 0 AndAlso
                       DuplicateIncludeCount = 0 AndAlso
                       BlockingGeneratedOutputCount = 0 AndAlso
                       ErrorCount = 0
            End Get
        End Property

        Friend ReadOnly Property Summary As String
            Get
                Return "Dead-path closure: " &
                       ProjectAssetCount.ToString(CultureInfo.InvariantCulture) & " declared project asset(s), " &
                       CompileAssetCount.ToString(CultureInfo.InvariantCulture) & " Compile item(s), " &
                       ActiveSourceFileCount.ToString(CultureInfo.InvariantCulture) & " active source file(s), " &
                       UnownedActiveSourceCount.ToString(CultureInfo.InvariantCulture) & " unowned active source file(s), " &
                       MissingDeclaredAssetCount.ToString(CultureInfo.InvariantCulture) & " missing declared asset(s), " &
                       DuplicateIncludeCount.ToString(CultureInfo.InvariantCulture) & " duplicate include(s), " &
                       BlockingGeneratedOutputCount.ToString(CultureInfo.InvariantCulture) & " blocking generated-output path(s), " &
                       MonitoredGeneratedOutputCount.ToString(CultureInfo.InvariantCulture) & " monitored generated-output path(s)."
            End Get
        End Property

        Friend Function CreateFindingDetails(maxItems As Integer) As IReadOnlyList(Of String)
            Dim details As New List(Of String)()
            Dim limit As Integer = Math.Max(1, maxItems)

            For Each finding As MASDeadPathClosureFinding In _findings
                If finding Is Nothing Then Continue For
                details.Add(finding.ToString())
                If details.Count >= limit Then Exit For
            Next

            Return New ReadOnlyCollection(Of String)(details)
        End Function

        Friend Function CreateSummaryDetails() As IReadOnlyList(Of String)
            Dim details As New List(Of String) From {
                Summary,
                "Project truth: Nexamas.UI.vbproj remains the single owner for active MASSystem, Component, and My Project VB source files.",
                "Source hygiene: declared project assets must exist and duplicate Include entries are blocking dead paths.",
                "Generated output hygiene: root-level build outputs are blocked; sample and .artifacts outputs remain monitored evidence only, not active source."
            }

            For Each finding As MASDeadPathClosureFinding In _findings
                If finding Is Nothing Then Continue For
                If finding.Severity <> MASDeadPathClosureFindingSeverity.Info AndAlso details.Count < 24 Then details.Add(finding.ToString())
            Next

            Return New ReadOnlyCollection(Of String)(details)
        End Function

        Private Function CountBySeverity(severity As MASDeadPathClosureFindingSeverity) As Integer
            Dim count As Integer = 0
            For Each finding As MASDeadPathClosureFinding In _findings
                If finding IsNot Nothing AndAlso finding.Severity = severity Then count += 1
            Next
            Return count
        End Function
    End Class

    ''' <summary>
    ''' File-system proof that the source tree has no unowned active VB source, missing declared assets,
    ''' duplicate project includes, or root-level generated-output paths masquerading as product source.
    ''' </summary>
    ''' <remarks>
    ''' This audit does not delete files, rewrite project items, execute build tools, capture render output, or create
    ''' a second source registry. It only verifies that the existing project file remains the single active source owner.
    ''' </remarks>
    Friend NotInheritable Class MASDeadPathClosureAudit
        Private Sub New()
        End Sub

        Friend Shared Function Evaluate(projectRoot As String) As MASDeadPathClosureReport
            Dim findings As New List(Of MASDeadPathClosureFinding)()
            Dim root As String = NormalizeRoot(projectRoot)

            If String.IsNullOrWhiteSpace(root) OrElse Not Directory.Exists(root) Then
                findings.Add(MASDeadPathClosureFinding.[Error]("MASDeadPathClosure.MissingProjectRoot", ".", "Project root is required for final dead-path closure."))
                Return New MASDeadPathClosureReport(0, 0, 0, 0, 0, 0, 0, 0, findings)
            End If

            Dim projectFile As String = Path.Combine(root, "Nexamas.UI.vbproj")
            If Not File.Exists(projectFile) Then
                findings.Add(MASDeadPathClosureFinding.[Error]("MASDeadPathClosure.MissingProjectFile", "Nexamas.UI.vbproj", "Nexamas.UI.vbproj must exist at the project root."))
                Return New MASDeadPathClosureReport(0, 0, 0, 0, 0, 0, 0, 0, findings)
            End If

            Dim projectAssets As IReadOnlyList(Of MASProjectAssetReference) = LoadProjectAssets(projectFile, findings)
            Dim compileAssets As IReadOnlyList(Of String) = FilterAssetPaths(projectAssets, "Compile")
            Dim compileSet As HashSet(Of String) = CreatePathSet(compileAssets)

            Dim duplicateIncludeCount As Integer = AddDuplicateIncludeFindings(projectAssets, findings)
            Dim missingDeclaredAssetCount As Integer = AddMissingDeclaredAssetFindings(root, projectAssets, findings)
            Dim activeSourceFiles As IReadOnlyList(Of String) = FindActiveSourceFiles(root)
            Dim unownedActiveSourceCount As Integer = AddUnownedActiveSourceFindings(activeSourceFiles, compileSet, findings)
            Dim blockingGeneratedOutputCount As Integer = 0
            Dim monitoredGeneratedOutputCount As Integer = 0
            AddGeneratedOutputFindings(root, findings, blockingGeneratedOutputCount, monitoredGeneratedOutputCount)

            If findings.Count = 0 Then
                findings.Add(MASDeadPathClosureFinding.Info(
                    "MASDeadPathClosure.Closed",
                    "ProjectRoot",
                    "The active source tree is closed: project-declared assets exist, Compile ownership is complete, no duplicate Includes were found, and no root-level generated-output path is masquerading as source."))
            ElseIf CountErrors(findings) = 0 Then
                findings.Add(MASDeadPathClosureFinding.Info(
                    "MASDeadPathClosure.ClosedWithMonitoring",
                    "ProjectRoot",
                    "No blocking dead paths were found; generated proof outputs remain monitored because they are outside the active Nexamas UI source tree."))
            End If

            Return New MASDeadPathClosureReport(projectAssets.Count,
                                                compileAssets.Count,
                                                activeSourceFiles.Count,
                                                unownedActiveSourceCount,
                                                missingDeclaredAssetCount,
                                                duplicateIncludeCount,
                                                blockingGeneratedOutputCount,
                                                monitoredGeneratedOutputCount,
                                                findings)
        End Function

        Private Shared Function LoadProjectAssets(projectFile As String,
                                                  findings As IList(Of MASDeadPathClosureFinding)) As IReadOnlyList(Of MASProjectAssetReference)
            Dim assets As New List(Of MASProjectAssetReference)()

            Try
                Dim document As XDocument = XDocument.Load(projectFile)
                Dim includeElements = document.Descendants().Where(Function(element) IsProjectAssetElement(element.Name.LocalName))

                For Each element As XElement In includeElements
                    Dim includeAttribute As XAttribute = element.Attribute("Include")
                    If includeAttribute Is Nothing Then Continue For

                    Dim relativePath As String = NormalizeRelativePath(includeAttribute.Value)
                    If relativePath.Length = 0 Then Continue For
                    If relativePath.Contains("$(") OrElse relativePath.Contains("*") Then Continue For

                    assets.Add(New MASProjectAssetReference(element.Name.LocalName, relativePath))
                Next
            Catch ex As Exception
                findings.Add(MASDeadPathClosureFinding.[Error]("MASDeadPathClosure.ProjectParseFailed", "Nexamas.UI.vbproj", If(ex.Message, String.Empty)))
            End Try

            Return New ReadOnlyCollection(Of MASProjectAssetReference)(assets)
        End Function

        Private Shared Function FilterAssetPaths(projectAssets As IEnumerable(Of MASProjectAssetReference),
                                                 itemKind As String) As IReadOnlyList(Of String)
            Dim paths As New List(Of String)()
            If projectAssets IsNot Nothing Then
                For Each asset As MASProjectAssetReference In projectAssets
                    If asset Is Nothing Then Continue For
                    If String.Equals(asset.ItemKind, itemKind, StringComparison.OrdinalIgnoreCase) Then paths.Add(asset.RelativePath)
                Next
            End If
            Return New ReadOnlyCollection(Of String)(paths)
        End Function

        Private Shared Function AddDuplicateIncludeFindings(projectAssets As IEnumerable(Of MASProjectAssetReference),
                                                            findings As IList(Of MASDeadPathClosureFinding)) As Integer
            Dim seen As New Dictionary(Of String, Integer)(StringComparer.OrdinalIgnoreCase)
            If projectAssets IsNot Nothing Then
                For Each asset As MASProjectAssetReference In projectAssets
                    If asset Is Nothing Then Continue For
                    Dim key As String = asset.ItemKind & "|" & asset.RelativePath
                    If seen.ContainsKey(key) Then
                        seen(key) += 1
                    Else
                        seen.Add(key, 1)
                    End If
                Next
            End If

            Dim duplicateCount As Integer = 0
            For Each pair As KeyValuePair(Of String, Integer) In seen
                If pair.Value <= 1 Then Continue For
                duplicateCount += 1
                findings.Add(MASDeadPathClosureFinding.[Error]("MASDeadPathClosure.DuplicateProjectInclude", pair.Key, "Duplicate project Include entries create parallel/dead ownership paths and must be merged."))
            Next

            Return duplicateCount
        End Function

        Private Shared Function AddMissingDeclaredAssetFindings(root As String,
                                                                projectAssets As IEnumerable(Of MASProjectAssetReference),
                                                                findings As IList(Of MASDeadPathClosureFinding)) As Integer
            Dim missingCount As Integer = 0
            If projectAssets Is Nothing Then Return missingCount

            For Each asset As MASProjectAssetReference In projectAssets
                If asset Is Nothing Then Continue For
                Dim fullPath As String = Path.Combine(root, asset.RelativePath.Replace("/"c, Path.DirectorySeparatorChar))
                If File.Exists(fullPath) Then Continue For
                missingCount += 1
                findings.Add(MASDeadPathClosureFinding.[Error]("MASDeadPathClosure.MissingDeclaredAsset", asset.RelativePath, "The project declares this asset, but the file is missing from the product tree."))
            Next

            Return missingCount
        End Function

        Private Shared Function FindActiveSourceFiles(root As String) As IReadOnlyList(Of String)
            Dim files As New List(Of String)()
            AddSourceFilesUnder(root, "MASSystem", files)
            AddSourceFilesUnder(root, "Component", files)
            AddSourceFilesUnder(root, "My Project", files)
            files.Sort(StringComparer.OrdinalIgnoreCase)
            Return New ReadOnlyCollection(Of String)(files)
        End Function

        Private Shared Sub AddSourceFilesUnder(root As String,
                                               relativeDirectory As String,
                                               files As IList(Of String))
            Dim sourceDirectory As String = System.IO.Path.Combine(root, relativeDirectory.Replace("/"c, System.IO.Path.DirectorySeparatorChar))
            If Not System.IO.Directory.Exists(sourceDirectory) Then Return

            For Each fullPath As String In System.IO.Directory.GetFiles(sourceDirectory, "*.vb", SearchOption.AllDirectories)
                If IsGeneratedOutputPath(root, fullPath) Then Continue For
                files.Add(ToRelativePath(root, fullPath))
            Next
        End Sub

        Private Shared Function AddUnownedActiveSourceFindings(activeSourceFiles As IEnumerable(Of String),
                                                               compileSet As HashSet(Of String),
                                                               findings As IList(Of MASDeadPathClosureFinding)) As Integer
            Dim unownedCount As Integer = 0
            If activeSourceFiles Is Nothing Then Return unownedCount

            For Each relativePath As String In activeSourceFiles
                If compileSet IsNot Nothing AndAlso compileSet.Contains(relativePath) Then Continue For
                unownedCount += 1
                findings.Add(MASDeadPathClosureFinding.[Error]("MASDeadPathClosure.UnownedActiveSource", relativePath, "Active Nexamas UI source files must be owned by Nexamas.UI.vbproj Compile entries."))
            Next

            Return unownedCount
        End Function

        Private Shared Sub AddGeneratedOutputFindings(root As String,
                                                      findings As IList(Of MASDeadPathClosureFinding),
                                                      ByRef blockingCount As Integer,
                                                      ByRef monitoredCount As Integer)
            blockingCount = 0
            monitoredCount = 0

            For Each fullDirectory As String In Directory.GetDirectories(root, "*", SearchOption.AllDirectories)
                Dim name As String = Path.GetFileName(fullDirectory)
                If Not String.Equals(name, "bin", StringComparison.OrdinalIgnoreCase) AndAlso
                   Not String.Equals(name, "obj", StringComparison.OrdinalIgnoreCase) Then Continue For

                Dim relativePath As String = ToRelativePath(root, fullDirectory)
                If IsAllowedGeneratedProofPath(relativePath) Then
                    monitoredCount += 1
                    findings.Add(MASDeadPathClosureFinding.Warning("MASDeadPathClosure.MonitoredGeneratedProofOutput", relativePath, "Generated output exists only under sample/.artifacts proof paths; keep it out of active source and package payloads."))
                Else
                    blockingCount += 1
                    findings.Add(MASDeadPathClosureFinding.[Error]("MASDeadPathClosure.BlockingGeneratedOutput", relativePath, "Root product generated-output paths must not sit beside active source as dead or parallel ownership paths."))
                End If
            Next
        End Sub

        Private Shared Function CreatePathSet(paths As IEnumerable(Of String)) As HashSet(Of String)
            Dim result As New HashSet(Of String)(StringComparer.OrdinalIgnoreCase)
            If paths Is Nothing Then Return result

            For Each path As String In paths
                Dim normalized As String = NormalizeRelativePath(path)
                If normalized.Length > 0 AndAlso Not result.Contains(normalized) Then result.Add(normalized)
            Next

            Return result
        End Function

        Private Shared Function CountErrors(findings As IEnumerable(Of MASDeadPathClosureFinding)) As Integer
            Dim count As Integer = 0
            If findings Is Nothing Then Return count

            For Each finding As MASDeadPathClosureFinding In findings
                If finding IsNot Nothing AndAlso finding.Severity = MASDeadPathClosureFindingSeverity.Error Then count += 1
            Next
            Return count
        End Function

        Private Shared Function IsProjectAssetElement(localName As String) As Boolean
            Return String.Equals(localName, "Compile", StringComparison.OrdinalIgnoreCase) OrElse
                   String.Equals(localName, "None", StringComparison.OrdinalIgnoreCase) OrElse
                   String.Equals(localName, "EmbeddedResource", StringComparison.OrdinalIgnoreCase) OrElse
                   String.Equals(localName, "Content", StringComparison.OrdinalIgnoreCase)
        End Function

        Private Shared Function IsGeneratedOutputPath(root As String,
                                                      fullPath As String) As Boolean
            Dim relativePath As String = ToRelativePath(root, fullPath)
            Return relativePath.IndexOf("/bin/", StringComparison.OrdinalIgnoreCase) >= 0 OrElse
                   relativePath.IndexOf("/obj/", StringComparison.OrdinalIgnoreCase) >= 0 OrElse
                   relativePath.EndsWith("/bin", StringComparison.OrdinalIgnoreCase) OrElse
                   relativePath.EndsWith("/obj", StringComparison.OrdinalIgnoreCase)
        End Function

        Private Shared Function IsAllowedGeneratedProofPath(relativePath As String) As Boolean
            Dim normalized As String = NormalizeRelativePath(relativePath)
            Return normalized.StartsWith("samples/", StringComparison.OrdinalIgnoreCase) OrElse
                   normalized.StartsWith(".artifacts/", StringComparison.OrdinalIgnoreCase) OrElse
                   normalized.StartsWith("eng/tests/", StringComparison.OrdinalIgnoreCase) OrElse
                   normalized.StartsWith("eng/benchmarks/", StringComparison.OrdinalIgnoreCase)
        End Function

        Private Shared Function NormalizeRoot(value As String) As String
            If String.IsNullOrWhiteSpace(value) Then Return String.Empty
            Return Path.GetFullPath(value.Trim())
        End Function

        Private Shared Function NormalizeRelativePath(value As String) As String
            If String.IsNullOrWhiteSpace(value) Then Return String.Empty
            Return value.Trim().Replace("\"c, "/"c)
        End Function

        Private Shared Function ToRelativePath(root As String,
                                               fullPath As String) As String
            Dim rootPath As String = NormalizeRoot(root)
            Dim filePath As String = Path.GetFullPath(fullPath)

            If Not rootPath.EndsWith(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal) Then rootPath &= Path.DirectorySeparatorChar
            If filePath.StartsWith(rootPath, StringComparison.OrdinalIgnoreCase) Then
                Return NormalizeRelativePath(filePath.Substring(rootPath.Length))
            End If

            Return NormalizeRelativePath(fullPath)
        End Function
    End Class

    Friend NotInheritable Class MASProjectAssetReference
        Friend Sub New(itemKind As String,
                       relativePath As String)
            Me.ItemKind = NormalizeText(itemKind)
            Me.RelativePath = NormalizeRelativePath(relativePath)
        End Sub

        Friend ReadOnly Property ItemKind As String
        Friend ReadOnly Property RelativePath As String

        Private Shared Function NormalizeText(value As String) As String
            Return If(value, String.Empty).Trim()
        End Function

        Private Shared Function NormalizeRelativePath(value As String) As String
            If String.IsNullOrWhiteSpace(value) Then Return String.Empty
            Return value.Trim().Replace("\"c, "/"c)
        End Function
    End Class

End Namespace
