#Requires -Version 5.1
<#
.SYNOPSIS
    Tracks user activity and reports working/stopped status to a central server.
.DESCRIPTION
    This PowerShell script monitors user idle time and reports state changes
    to a central Flask API. It runs at user logon via Task Scheduler and
    maintains a state machine between "working" and "stopped" states based
    on a 6-minute inactivity threshold.
.NOTES
    File Name  : report.ps1
    Author     : IT Department
    Version    : 1.2
#>

# Hide the PowerShell console window
Add-Type -Name Window -Namespace Console -MemberDefinition '
[DllImport("Kernel32.dll")]
public static extern IntPtr GetConsoleWindow();
[DllImport("user32.dll")]
public static extern bool ShowWindow(IntPtr hWnd, Int32 nCmdShow);
'
$consolePtr = [Console.Window]::GetConsoleWindow()
[void][Console.Window]::ShowWindow($consolePtr, 0) # 0 = hide

# Configuration
$ApiUrl = "http://localhost:5000/api/report" # Default value, can be overridden by environment variable
$IdleThresholdMinutes = 10 # User idle time in minutes before state changes to 'stopped'.
$LogFilePath = Join-Path $env:TEMP "user_work_tracking_client.log"
$pollIntervalSeconds = 60  # How often to check user activity (in seconds).
$reportIntervalMinutes = 1 # How often to send a status update if state hasn't changed (in minutes). With a 1-minute poll interval, this ensures 'working' state is reported every minute. State changes are reported immediately.
$lastReportTime = [DateTime]::MinValue # Initialize last report time

# Helper Function for Logging
function Write-Log($Message) {
    $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
    try {
        "$timestamp - $Message" | Out-File -Append -FilePath $LogFilePath -Encoding UTF8 -ErrorAction Stop
    } catch {
        Write-Warning "Failed to write to log file '$LogFilePath': $($_.Exception.Message)"
        # Optionally, write to console as fallback
        Write-Host "$timestamp [LOG FALLBACK] - $Message"
    }
}

Write-Log "================ Script Started ==================="

# Try to read config from environment or a local config file
try {
    # Check for environment variable first
    if ($env:API_ENDPOINT) {
        $ApiUrl = $env:API_ENDPOINT
        Write-Log "Using API URL from environment variable: $ApiUrl"
    }
    else {
        # Look for a config file
        $configPath = Join-Path ([System.IO.Path]::GetDirectoryName($PSCommandPath)) "config.env"
        if (Test-Path $configPath) {
            Write-Log "Found config file at $configPath"
            Get-Content $configPath | ForEach-Object {
                if ($_ -match '^API_ENDPOINT=(.*)$') {
                    $ApiUrl = $matches[1].Trim('"')
                    Write-Log "Using API URL from config file: $ApiUrl"
                }
                if ($_ -match '^IDLE_THRESHOLD_MINUTES=(\d+)$') {
                    $IdleThresholdMinutes = [int]$matches[1]
                    Write-Log "Using idle threshold from config file: $IdleThresholdMinutes minutes"
                }
                if ($_ -match '^POLL_INTERVAL_SECONDS=(\d+)$') {
                    $pollIntervalSeconds = [int]$matches[1]
                    Write-Log "Using poll interval from config file: $pollIntervalSeconds seconds"
                }
                if ($_ -match '^REPORT_INTERVAL_MINUTES=(\d+)$') {
                    $reportIntervalMinutes = [int]$matches[1]
                    Write-Log "Using report interval from config file: $reportIntervalMinutes minutes"
                }
            }
        }
        else {
            Write-Log "No config file found. Using default values."
        }
    }
}
catch {
    Write-Log "Error reading configuration: $_. Using default values."
}

Write-Log "Using API URL: $ApiUrl"
Write-Log "Idle Threshold (Minutes): $IdleThresholdMinutes"
Write-Log "Report Interval (Minutes): $reportIntervalMinutes"

# Initiate state
$currentState = "working"  # Start in working state (user just logged in)
$lastReportedState = $null

# Function to get idle time using quser command
function Get-IdleTime {
    try {
        # First try the Win32 API method (more reliable)
        if (-not ([System.Management.Automation.PSTypeName]'IdleTime').Type) {
            Add-Type @'
using System;
using System.Runtime.InteropServices;

public class IdleTime {
    [DllImport("user32.dll")]
    public static extern bool GetLastInputInfo(ref LASTINPUTINFO plii);

    public static TimeSpan GetIdleTime() {
        LASTINPUTINFO lastInput = new LASTINPUTINFO();
        lastInput.cbSize = (uint)Marshal.SizeOf(lastInput);
        
        if (GetLastInputInfo(ref lastInput)) {
            uint currentTickCount = (uint)Environment.TickCount;
            uint idleTicks = currentTickCount - lastInput.dwTime;
            return TimeSpan.FromMilliseconds(idleTicks);
        } else {
            return TimeSpan.Zero;
        }
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct LASTINPUTINFO {
        public uint cbSize;
        public uint dwTime;
    }
}
'@
        }

        # Use the Win32 API method
        $idleTime = [IdleTime]::GetIdleTime()
        $idleMinutes = $idleTime.TotalMinutes
        Write-Log "Win32 API reports idle time: $idleMinutes minutes"
        return $idleTime
    }
    catch {
        Write-Log "Error using Win32 API for idle time: $_. Falling back to quser"
        
        # Fallback: Try the quser command method
        try {
            $quser = quser $env:USERNAME | Select-Object -Skip 1
            $idleText = ($quser -replace '\s{2,}', ',').Split(',')[3]
            
            # If no idle time is reported, return 0
            if ($idleText -eq "." -or $idleText -eq "none" -or $idleText -eq $null) {
                Write-Log "quser reports no idle time"
                return [TimeSpan]::Zero
            }
            
            # Parse HH:MM format
            if ($idleText -match '(\d+):(\d+)') {
                $hours = [int]$Matches[1]
                $minutes = [int]$Matches[2]
                $result = [TimeSpan]::FromMinutes(($hours * 60) + $minutes)
                Write-Log "quser parsed idle time: $($result.TotalMinutes) minutes"
                return $result
            }
            
            # Parse "MM+" format (represents minutes)
            if ($idleText -match '(\d+)\+') {
                $minutes = [int]$Matches[1]
                $result = [TimeSpan]::FromMinutes($minutes)
                Write-Log "quser parsed idle time: $($result.TotalMinutes) minutes"
                return $result
            }
            
            # Default to zero if we couldn't parse
            Write-Log "quser couldn't parse idle format: '$idleText'"
            return [TimeSpan]::Zero
        }
        catch {
            Write-Log "Error getting idle time via quser: $_"
            return [TimeSpan]::Zero
        }
    }
}

# Function to report state to the API
function Send-StateReport {
    param (
        [string]$State
    )
    
    # Generate timestamp in UTC format to ensure time sync with server
    # The server processes all timestamps in UTC
    $utcTimestamp = (Get-Date).ToUniversalTime().ToString("o") # ISO 8601 format
    
    $payload = @{
        user = $env:USERNAME
        state = $State
        ts = $utcTimestamp
    } | ConvertTo-Json

    Write-Log "Preparing to send payload with UTC timestamp: $utcTimestamp"
    Write-Log "Full payload: $payload"

    try {
        Write-Log "Attempting API call to $ApiUrl"
        # Send to API
        $response = Invoke-RestMethod -Uri $ApiUrl `
                                      -Method Post `
                                      -Body $payload `
                                      -ContentType "application/json" `
                                      -ErrorAction Stop
        
        Write-Log "API call successful. Response: $($response | ConvertTo-Json -Depth 3)"
        
        # Update last report time
        $script:lastReportTime = Get-Date
        
        # Check for success field in response
        if ($response.success -eq $true) {
            Write-Log "State '$State' reported successfully."
            return $true
        } else {
            Write-Log "API indicated failure. Message: $($response.message)"
            return $false
        }
    }
    catch {
        # Log error details
        $errorMessage = "Failed to report state '$State' to API. Error: $($_.Exception.Message)"
        if ($_.Exception.Response) {
            $statusCode = $_.Exception.Response.StatusCode
            $statusDescription = $_.Exception.Response.StatusDescription
            $errorMessage += " Status Code: $statusCode ($statusDescription)"
            try {
                $responseBody = $_.Exception.Response.GetResponseStream()
                $streamReader = New-Object System.IO.StreamReader($responseBody)
                $errorBody = $streamReader.ReadToEnd()
                $streamReader.Close()
                $responseBody.Close()
                $errorMessage += " Response Body: $errorBody"
            } catch {
                $errorMessage += " (Could not read error response body)"
            }
        }
        Write-Log $errorMessage
        return $false
    }
}

# Initial state report at startup (user just logged in = working)
if (Send-StateReport -State $currentState) {
    $lastReportedState = $currentState
    Write-Log "Initial state reported as '$currentState'"
} else {
    Write-Log "Failed to report initial state. Will retry on next check."
}

# Main monitoring loop
Write-Log "Starting activity monitoring with $IdleThresholdMinutes minute idle threshold"

try {
    while ($true) {
        # Get current idle time
        $idleTime = Get-IdleTime
        $idleMinutes = $idleTime.TotalMinutes
        Write-Log "Idle time check: $idleMinutes minutes."

        # If idle time couldn't be determined, log and wait
        if ($idleTime -lt 0) {
            Write-Log "Error getting idle time. Waiting for next check."
            Start-Sleep -Seconds $pollIntervalSeconds
            continue
        }
        
        # Determine state based on idle time
        $newState = if ($idleMinutes -ge $IdleThresholdMinutes) { "stopped" } else { "working" }
        Write-Log "Determined state: $newState (Current: $currentState)"

        # Check if it's time to send a periodic report (every 1 minute)
        $timeSinceLastReport = (Get-Date) - $lastReportTime
        $shouldSendPeriodicReport = $timeSinceLastReport.TotalMinutes -ge $reportIntervalMinutes
        
        Write-Log "Time since last report: $($timeSinceLastReport.TotalMinutes.ToString("F2")) minutes (threshold: $reportIntervalMinutes)"

        # If state changed or it's time for periodic report, send it
        if ($newState -ne $currentState -or $shouldSendPeriodicReport) {
            # If it's a state change, update current state
            if ($newState -ne $currentState) {
                Write-Log "State changed from '$currentState' to '$newState'. Reporting to API."
                $currentState = $newState
            } else {
                Write-Log "Sending periodic report (current state: $currentState)"
            }
            
            # Report to API
            try {
                if (Send-StateReport -State $currentState) {
                    $lastReportedState = $currentState
                }
            }
            catch {
                Write-Log "Error reporting state: $_"
            }
        }
        
        # Sleep until next check
        Start-Sleep -Seconds $pollIntervalSeconds
    }
}
catch {
    Write-Log "Critical error in main loop: $_. Script will attempt to report 'stopped' state in finally block."
    exit 1 # Exit with an error code, but finally block will still run
}
finally {
    Write-Log "Script ending (e.g., user logoff, task stop, or after error). Attempting to report 'stopped' state."
    Send-StateReport -State "stopped"
    Write-Log "Final 'stopped' state report attempt made."
    Write-Log "================ Script Ended (via Finally) ================"
} 