[Console]::OutputEncoding = [System.Text.Encoding]::UTF8

function Initialize-BrowserAutomationRuntime {
    if (-not ('System.Windows.Forms.Application' -as [type])) {
        Add-Type -AssemblyName System.Windows.Forms
    }

    if (-not ('System.Drawing.Bitmap' -as [type])) {
        Add-Type -AssemblyName System.Drawing
    }

    [System.Windows.Forms.Application]::EnableVisualStyles()
}

function Assert-BrowserAutomationSta {
    $apartmentState = [System.Threading.Thread]::CurrentThread.ApartmentState
    if ($apartmentState -ne [System.Threading.ApartmentState]::STA) {
        throw 'Browser automation ต้องรันใน STA apartment. ให้ใช้ Windows PowerShell ปกติหรือ powershell.exe -Sta'
    }
}

function Get-BrowserArtifactDirectory {
    param([string]$ArtifactDirectory = '')

    if (-not [string]::IsNullOrWhiteSpace($ArtifactDirectory)) {
        New-Item -ItemType Directory -Path $ArtifactDirectory -Force | Out-Null
        return (Resolve-Path -LiteralPath $ArtifactDirectory).Path
    }

    $defaultRoot = Join-Path (Split-Path -Path $PSScriptRoot -Parent) 'storage\maintenance_exports\browser-spot-check'
    $timestamp = Get-Date -Format 'yyyyMMdd-HHmmss'
    $path = Join-Path $defaultRoot $timestamp
    New-Item -ItemType Directory -Path $path -Force | Out-Null
    return (Resolve-Path -LiteralPath $path).Path
}

function Resolve-BrowserBaseUrl {
    param([string]$BaseUrl)

    return ($BaseUrl.TrimEnd('/'))
}

function Invoke-BrowserMessagePump {
    param([int]$SleepMilliseconds = 100)

    [System.Windows.Forms.Application]::DoEvents()
    Start-Sleep -Milliseconds $SleepMilliseconds
}

function Wait-BrowserUntil {
    param(
        [scriptblock]$Condition,
        [int]$TimeoutSeconds = 20,
        [int]$PollMilliseconds = 100,
        [string]$FailureMessage = 'Browser condition was not satisfied in time.'
    )

    $deadline = (Get-Date).AddSeconds($TimeoutSeconds)
    while ((Get-Date) -lt $deadline) {
        try {
            if ([bool](& $Condition)) {
                return $true
            }
        } catch {
        }

        Invoke-BrowserMessagePump -SleepMilliseconds $PollMilliseconds
    }

    throw $FailureMessage
}

function Start-BrowserAutomationSession {
    param(
        [string]$StartUrl = 'about:blank',
        [string]$ArtifactDirectory = '',
        [int]$Width = 1440,
        [int]$Height = 1100
    )

    Assert-BrowserAutomationSta
    Initialize-BrowserAutomationRuntime

    $artifactRoot = Get-BrowserArtifactDirectory -ArtifactDirectory $ArtifactDirectory
    $offscreenX = [Math]::Max([System.Windows.Forms.SystemInformation]::VirtualScreen.Right + 50, 2000)

    $form = New-Object System.Windows.Forms.Form
    $form.Width = $Width
    $form.Height = $Height
    $form.StartPosition = [System.Windows.Forms.FormStartPosition]::Manual
    $form.Location = New-Object System.Drawing.Point($offscreenX, 0)
    $form.ShowInTaskbar = $false
    $form.MaximizeBox = $false
    $form.MinimizeBox = $false
    $form.Text = 'CCW Browser Automation'

    $browser = New-Object System.Windows.Forms.WebBrowser
    $browser.ScriptErrorsSuppressed = $true
    $browser.Dock = [System.Windows.Forms.DockStyle]::Fill
    $form.Controls.Add($browser)
    $form.Show()
    Invoke-BrowserMessagePump -SleepMilliseconds 100

    $session = @{
        Form = $form
        Browser = $browser
        ArtifactDirectory = $artifactRoot
    }

    if (-not [string]::IsNullOrWhiteSpace($StartUrl)) {
        $browser.Navigate($StartUrl)
        $null = Wait-BrowserReady -Session $session -TimeoutSeconds 20
    }

    return $session
}

function Stop-BrowserAutomationSession {
    param([hashtable]$Session)

    if ($null -eq $Session) {
        return
    }

    if ($null -ne $Session.Form) {
        try {
            $Session.Form.Close()
        } catch {
        }

        try {
            $Session.Form.Dispose()
        } catch {
        }
    }
}

function Wait-BrowserReady {
    param(
        [hashtable]$Session,
        [int]$TimeoutSeconds = 20
    )

    $null = Wait-BrowserUntil -TimeoutSeconds $TimeoutSeconds -FailureMessage 'Browser page did not finish loading in time.' -Condition {
        $browser = $Session.Browser
        return (
            $null -ne $browser.Document -and
            $browser.ReadyState -eq [System.Windows.Forms.WebBrowserReadyState]::Complete
        )
    }

    Invoke-BrowserMessagePump -SleepMilliseconds 250
}

function Wait-BrowserUrlContains {
    param(
        [hashtable]$Session,
        [string]$ExpectedFragment,
        [int]$TimeoutSeconds = 20
    )

    $null = Wait-BrowserUntil -TimeoutSeconds $TimeoutSeconds -FailureMessage "Browser URL did not contain '$ExpectedFragment' in time." -Condition {
        $currentUrl = Get-BrowserCurrentUrl -Session $Session
        return $currentUrl.Contains($ExpectedFragment)
    }

    $null = Wait-BrowserReady -Session $Session -TimeoutSeconds $TimeoutSeconds
}

function Get-BrowserCurrentUrl {
    param([hashtable]$Session)

    if ($null -eq $Session.Browser.Url) {
        return ''
    }

    return [string]$Session.Browser.Url.AbsoluteUri
}

function Get-BrowserPageTitle {
    param([hashtable]$Session)

    if ($null -eq $Session.Browser.Document) {
        return ''
    }

    return [string]$Session.Browser.Document.Title
}

function Get-BrowserBodyText {
    param([hashtable]$Session)

    if ($null -eq $Session.Browser.Document -or $null -eq $Session.Browser.Document.Body) {
        return ''
    }

    $bodyText = $Session.Browser.Document.Body.InnerText
    if ($null -eq $bodyText) {
        return ''
    }

    return [string]$bodyText
}

function Get-BrowserDocumentHtml {
    param([hashtable]$Session)

    $documentText = $Session.Browser.DocumentText
    if ($null -eq $documentText) {
        return ''
    }

    return [string]$documentText
}

function Navigate-BrowserPage {
    param(
        [hashtable]$Session,
        [string]$Url,
        [int]$TimeoutSeconds = 20
    )

    $Session.Browser.Navigate($Url)
    $null = Wait-BrowserUntil -TimeoutSeconds $TimeoutSeconds -FailureMessage "Browser did not navigate to $Url in time." -Condition {
        $currentUrl = Get-BrowserCurrentUrl -Session $Session
        return $currentUrl -eq $Url -and $Session.Browser.ReadyState -eq [System.Windows.Forms.WebBrowserReadyState]::Complete
    }

    Invoke-BrowserMessagePump -SleepMilliseconds 300
}

function Save-BrowserScreenshot {
    param(
        [hashtable]$Session,
        [string]$OutputPath
    )

    $directory = Split-Path -Path $OutputPath -Parent
    if ($directory -ne '' -and -not (Test-Path -LiteralPath $directory)) {
        New-Item -ItemType Directory -Path $directory -Force | Out-Null
    }

    Invoke-BrowserMessagePump -SleepMilliseconds 300
    $width = [Math]::Max($Session.Form.ClientSize.Width, 1)
    $height = [Math]::Max($Session.Form.ClientSize.Height, 1)
    $bitmap = New-Object System.Drawing.Bitmap($width, $height)
    try {
        $Session.Form.DrawToBitmap($bitmap, [System.Drawing.Rectangle]::FromLTRB(0, 0, $width, $height))
        $bitmap.Save($OutputPath, [System.Drawing.Imaging.ImageFormat]::Png)
    } finally {
        $bitmap.Dispose()
    }

    return $OutputPath
}

function Invoke-BrowserEvaluate {
    param(
        [hashtable]$Session,
        [string]$Expression
    )

    if ($null -eq $Session.Browser.Document) {
        return $null
    }

    return $Session.Browser.Document.InvokeScript('eval', @($Expression))
}

function Get-BrowserElementsByClassName {
    param(
        [hashtable]$Session,
        [string]$ClassName
    )

    $matches = New-Object System.Collections.Generic.List[System.Windows.Forms.HtmlElement]
    if ($null -eq $Session.Browser.Document) {
        return $matches.ToArray()
    }

    foreach ($element in $Session.Browser.Document.All) {
        $classAttribute = $element.GetAttribute('className')
        $classes = if ($null -eq $classAttribute) { '' } else { [string]$classAttribute }
        if ([string]::IsNullOrWhiteSpace($classes)) {
            continue
        }

        foreach ($classPart in ($classes -split '\s+')) {
            if ($classPart -eq $ClassName) {
                $matches.Add($element) | Out-Null
                break
            }
        }
    }

    return $matches.ToArray()
}

function Test-BrowserHasClassName {
    param(
        [hashtable]$Session,
        [string]$ClassName
    )

    return @((Get-BrowserElementsByClassName -Session $Session -ClassName $ClassName)).Count -gt 0
}

function Get-BrowserFirstLinkHrefContaining {
    param(
        [hashtable]$Session,
        [string]$Fragment
    )

    if ($null -eq $Session.Browser.Document) {
        return ''
    }

    foreach ($link in $Session.Browser.Document.GetElementsByTagName('a')) {
        $hrefAttribute = $link.GetAttribute('href')
        $href = if ($null -eq $hrefAttribute) { '' } else { [string]$hrefAttribute }
        if ($href.Contains($Fragment)) {
            return $href
        }
    }

    return ''
}

function Get-BrowserFormCsrfToken {
    param(
        [hashtable]$Session,
        [string]$FormActionSuffix
    )

    $documentHtml = Get-BrowserDocumentHtml -Session $Session
    if ([string]::IsNullOrWhiteSpace($documentHtml)) {
        return ''
    }

    $forms = [regex]::Matches(
        $documentHtml,
        '<form\b[^>]*action=(["''])(?<action>[^"'']+)\1[^>]*>(?<body>.*?)</form>',
        [System.Text.RegularExpressions.RegexOptions]::IgnoreCase -bor [System.Text.RegularExpressions.RegexOptions]::Singleline
    )
    foreach ($formMatch in $forms) {
        $action = [System.Net.WebUtility]::HtmlDecode($formMatch.Groups['action'].Value)
        if (-not $action.EndsWith($FormActionSuffix) -and -not $action.Contains($FormActionSuffix.TrimStart('.'))) {
            continue
        }

        $csrfMatch = [regex]::Match(
            $formMatch.Groups['body'].Value,
            'name=["'']csrf_token["''][^>]*value=["'']([^"'']+)["'']',
            [System.Text.RegularExpressions.RegexOptions]::IgnoreCase
        )
        if ($csrfMatch.Success) {
            return [System.Net.WebUtility]::HtmlDecode($csrfMatch.Groups[1].Value)
        }
    }

    return ''
}

function ConvertTo-UrlEncodedPostBytes {
    param([hashtable]$FormData)

    $pairs = foreach ($entry in $FormData.GetEnumerator()) {
        $name = [System.Uri]::EscapeDataString([string]$entry.Key)
        $value = [System.Uri]::EscapeDataString([string]$entry.Value)
        '{0}={1}' -f $name, $value
    }

    return [System.Text.Encoding]::ASCII.GetBytes(($pairs -join '&'))
}

function Submit-BrowserLogin {
    param(
        [hashtable]$Session,
        [string]$BaseUrl,
        [string]$Username,
        [string]$Password,
        [string]$DisplayMode = 'web'
    )

    $resolvedBaseUrl = Resolve-BrowserBaseUrl -BaseUrl $BaseUrl
    $loginUrl = "$resolvedBaseUrl/pages/login.php"
    Navigate-BrowserPage -Session $Session -Url $loginUrl -TimeoutSeconds 20

    $csrfToken = Get-BrowserFormCsrfToken -Session $Session -FormActionSuffix '../process/login.php'
    if ([string]::IsNullOrWhiteSpace($csrfToken)) {
        throw 'ไม่พบ CSRF token ของฟอร์ม login ใน browser automation'
    }

    $postData = ConvertTo-UrlEncodedPostBytes -FormData @{
        csrf_token = $csrfToken
        username = $Username
        password = $Password
        display_mode = $DisplayMode
    }

    $headers = "Content-Type: application/x-www-form-urlencoded`r`n"
    $Session.Browser.Navigate("$resolvedBaseUrl/process/login.php", $null, $postData, $headers)

    $null = Wait-BrowserUntil -TimeoutSeconds 20 -FailureMessage 'Browser login did not finish in time.' -Condition {
        $currentUrl = Get-BrowserCurrentUrl -Session $Session
        return (
            $currentUrl -ne '' -and
            -not $currentUrl.EndsWith('/pages/login.php') -and
            $Session.Browser.ReadyState -eq [System.Windows.Forms.WebBrowserReadyState]::Complete
        )
    }

    Invoke-BrowserMessagePump -SleepMilliseconds 300
}
