---
- name: Развернуть Windows/RDP collector'ы AWatch-rus
  hosts: aw_windows
  gather_facts: false

  vars:
    ansible_winrm_operation_timeout_sec: 120
    ansible_winrm_read_timeout_sec: 180
    aw_windows_repo_root: "{{ playbook_dir | dirname }}"
    aw_windows_deploy_root: "C:\\Program Files\\AWatch-rus"
    aw_windows_rust_target_root: "{{ lookup('env', 'CARGO_TARGET_DIR') | default('/tmp/detmir-adk-rust-target', true) }}"
    aw_windows_telemetry_exe_source_candidates:
      - "{{ aw_windows_repo_root }}/windows/aw-windows-telemetry.exe"
      - "{{ aw_windows_rust_target_root }}/x86_64-pc-windows-gnu/release/aw-windows-telemetry.exe"
    aw_windows_telemetry_exe_source: ""
    aw_windows_server_scheme: "http"
    aw_windows_server_port: 5600
    aw_windows_package_version: "v0.13.2"
    aw_windows_package_url: "https://github.com/ActivityWatch/activitywatch/releases/download/v0.13.2/activitywatch-v0.13.2-windows-x86_64.zip"
    aw_windows_package_zip_path: ""
    aw_windows_domain: "SHARKON2025"
    aw_windows_builtin_administrator_name: "Администратор"
    aw_windows_users:
      - Администратор
      - user1
      - user2
      - user3
      - user4
      - user5
    aw_windows_extra_users: []
    aw_windows_users_effective: "{{ (aw_windows_users + aw_windows_extra_users) | unique }}"
    aw_windows_install_root: "C:\\Program Files\\AWatch-rus\\bin"
    aw_windows_state_root: "C:\\ProgramData\\AWatch-rus"
    aw_windows_policy_mode: "server"
    aw_windows_policy_refresh_seconds: 300
    aw_windows_policy_engine_enabled: true
    aw_windows_policy_engine_port: 5601
    aw_windows_policy_engine_scheme: "http"
    aw_windows_hayabusa_auto_upload_enabled: true
    aw_windows_hayabusa_auto_upload_interval_hours: 6
    aw_windows_hayabusa_auto_upload_hours_back: 6
    aw_windows_hayabusa_auto_upload_mode: "incident"
    aw_windows_hayabusa_auto_upload_task_name: "ActivityWatch Hayabusa Upload"
    aw_windows_hayabusa_auto_upload_run_as_user: ""
    aw_windows_file_1c_auto_upload_enabled: true
    aw_windows_file_1c_auto_upload_interval_minutes: 15
    aw_windows_file_1c_auto_upload_task_name: "ActivityWatch File1C Upload"
    aw_windows_file_1c_auto_upload_run_as_user: "{{ aw_windows_domain }}\\{{ aw_windows_builtin_administrator_name }}"
    aw_windows_file_1c_target_user: "igor"
    aw_windows_file_1c_registry_workbook_path: "E:\\USER1\\СПИСОК ПРЕДПРИЯТИЙ И ИХ РАСПРЕДЕЛЕНИЕ.xlsx"
    aw_windows_afk_enabled_default: true
    aw_windows_window_enabled_default: true
    aw_windows_file_ops_enabled: true
    aw_windows_local_agent_logs_enabled: false
    aw_windows_incident_capture_enabled: true
    aw_windows_incident_screenshot_enabled: true
    aw_windows_incident_artifacts_root: "{{ aw_windows_state_root }}\\incident-artifacts"
    aw_windows_forensics_root: "{{ aw_windows_state_root }}\\forensics\\evtx-exports"
    aw_windows_logon_marker_enabled: true
    aw_windows_process_events_enabled: false
    aw_windows_skip_hardening: false
    aw_windows_rules_path: "{{ aw_windows_deploy_root }}\\windows\\web-category-rules.example.json"
    aw_windows_policy_path: "{{ aw_windows_deploy_root }}\\windows\\dlp-policy.example.json"
    aw_windows_validation_remote_path: "{{ aw_windows_state_root }}\\aw_validate_ansible.json"
    aw_windows_validation_local_dir: "/tmp/aw-rus-validation-{{ lookup('env','USER') | default('ansible', true) }}"
    aw_windows_launch_task_pattern: "ActivityWatch Launch *"
    aw_windows_recovery_task_name: "ActivityWatch Recovery"
    aw_windows_collector_guard_enabled: true
    aw_windows_collector_guard_mode: "enforce"
    aw_windows_collector_guard_service_name: "AWatchRusCollectorGuard"
    aw_windows_collector_guard_loop_seconds: 60
    aw_windows_force_task_restart: true
    aw_windows_api_smoke_check_enabled: true
    aw_windows_api_smoke_check_bucket: ""
    aw_windows_api_smoke_check_limit: 10
    aw_windows_api_smoke_check_window_enabled_default: true
    aw_windows_api_smoke_check_window_bucket: ""
    aw_windows_api_smoke_check_min_events: 1
    aw_windows_fail_on_validation_error: true
    aw_windows_migration_enabled: true
    aw_windows_legacy_install_root: "C:\\Program Files\\ActivityWatch-Phase2"
    aw_windows_legacy_state_root: "C:\\ProgramData\\ActivityWatch-Phase2"
    aw_windows_migration_report_remote_path: "{{ aw_windows_state_root }}\\aw_migration_ansible.json"

  tasks:
    - name: Вычислить inventory host AW server по умолчанию
      ansible.builtin.set_fact:
        aw_server_inventory_host_effective: "{{ (groups['aw_server'] | default([]) | first) | default('', true) }}"

    - name: Вычислить inventory host analytics node по умолчанию
      ansible.builtin.set_fact:
        aw_analytics_inventory_host_effective: "{{ (groups['proxmox'] | default([]) | first) | default('', true) }}"

    - name: Вычислить effective host для AW server
      ansible.builtin.set_fact:
        aw_windows_server_host_effective: >-
          {{
            aw_windows_server_host
            | default(
                (
                  hostvars[aw_server_inventory_host_effective].ansible_host
                  | default(aw_server_inventory_host_effective, true)
                )
                if (aw_server_inventory_host_effective | length) > 0
                else '',
                true
              )
          }}

    - name: Вычислить effective каталог health validation на AW server
      ansible.builtin.set_fact:
        aw_windows_health_validation_dir_effective: >-
          {{
            (
              hostvars[aw_server_inventory_host_effective].aw_rus_health_validation_dir
              | default('/var/lib/activitywatch/health/windows-validation', true)
            )
            if (aw_server_inventory_host_effective | length) > 0
            else ''
          }}

    - name: Вычислить effective host для policy engine
      ansible.builtin.set_fact:
        aw_windows_policy_engine_host_effective: >-
          {{
            aw_windows_policy_engine_host
            | default(aw_windows_server_host_effective, true)
          }}

    - name: Вычислить effective host для file-1C analytics
      ansible.builtin.set_fact:
        aw_windows_file_1c_target_host_effective: >-
          {{
            aw_windows_file_1c_target_host
            | default(
                (
                  hostvars[aw_analytics_inventory_host_effective].ansible_host
                  | default(aw_analytics_inventory_host_effective, true)
                )
                if (aw_analytics_inventory_host_effective | length) > 0
                else '',
                true
              )
          }}

    - name: Проверить обязательные переменные
      ansible.builtin.assert:
        that:
          - aw_windows_server_host_effective | length > 0
          - aw_windows_server_port is defined
          - aw_windows_server_scheme is defined
          - aw_windows_domain is defined
          - aw_windows_builtin_administrator_name is defined
          - aw_windows_builtin_administrator_name | length > 0
          - aw_windows_users_effective | length > 0
          - aw_windows_install_root is defined
          - aw_windows_state_root is defined
          - (not (aw_windows_file_1c_auto_upload_enabled | bool)) or (aw_windows_file_1c_target_host_effective | length > 0)
        fail_msg: "Не заданы обязательные переменные Windows-развёртывания."

    - name: Нормализовать effective флаги collector'ов и smoke-check
      ansible.builtin.set_fact:
        aw_windows_afk_enabled_effective: "{{ (aw_windows_afk_enabled | default(aw_windows_afk_enabled_default)) | bool }}"
        aw_windows_window_enabled_effective: "{{ (aw_windows_window_enabled | default(aw_windows_window_enabled_default)) | bool }}"
        aw_windows_api_smoke_check_window_enabled_effective: "{{ (aw_windows_api_smoke_check_window_enabled | default(aw_windows_api_smoke_check_window_enabled_default)) | bool }}"

    - name: Создать каталоги развёртывания
      ansible.windows.win_file:
        path: "{{ item }}"
        state: directory
      loop:
        - "{{ aw_windows_deploy_root }}"
        - "{{ aw_windows_deploy_root }}\\windows"

    - name: Загрузить Windows toolkit развёртывания (без PS-дубликатов — заменены Rust)
      ansible.windows.win_copy:
        src: "{{ aw_windows_repo_root }}/windows/{{ item }}"
        dest: "{{ aw_windows_deploy_root }}\\windows\\{{ item }}"
      loop:
        - ActivityWatch.Windows.Common.psd1
        - ActivityWatch.Windows.Common.psm1
        - dlp-policy-client.ps1
        - email-outbound-collector.ps1
        - worktime-session-collector.ps1
        - export-evtx-for-hayabusa.ps1
        - export-upload-hayabusa-to-aw-server.ps1
        - export-upload-file-1c-telemetry.ps1
        - sync-dlp-evidence-artifacts.ps1
        - migrate-awatch-rus-paths.ps1
        - deploy-domain-users.ps1
        - deploy-ensemble.ps1
        - hardening-recovery.ps1
        - AWatchRusCollectorGuardService.cs
        - install-collector-guard-service.ps1
        - rebuild-worktime-tasks.ps1
        - audit-cryptopro.ps1
        - validate-deployment.ps1
        - web-category-rules.example.json
        - dlp-policy.example.json

    - name: Проверить кандидаты Rust Windows telemetry binary
      ansible.builtin.stat:
        path: "{{ item }}"
      loop: "{{ aw_windows_telemetry_exe_source_candidates }}"
      delegate_to: localhost
      become: false
      register: aw_windows_telemetry_exe_candidate_stats

    - name: Выбрать Rust Windows telemetry binary
      ansible.builtin.set_fact:
        aw_windows_telemetry_exe_source_effective: >-
          {{
            aw_windows_telemetry_exe_source
            if (aw_windows_telemetry_exe_source | string | length) > 0
            else (
              aw_windows_telemetry_exe_candidate_stats.results
              | selectattr('stat.exists')
              | map(attribute='item')
              | list
              | first
              | default('')
            )
          }}

    - name: Проверить наличие Rust Windows telemetry binary
      ansible.builtin.fail:
        msg: >-
          Не найден aw-windows-telemetry.exe. Соберите его командой:
          cargo build --release --target x86_64-pc-windows-gnu -p aw-windows-telemetry
          или используйте актуальный install-kit с windows/aw-windows-telemetry.exe.
      when: aw_windows_telemetry_exe_source_effective | length == 0

    - name: Загрузить Rust Windows telemetry binary
      ansible.windows.win_copy:
        src: "{{ aw_windows_telemetry_exe_source_effective }}"
        dest: "{{ aw_windows_deploy_root }}\\windows\\aw-windows-telemetry.exe"

    - name: Нормализовать кодировку PowerShell файлов (UTF-8 BOM для Windows PowerShell)
      ansible.windows.win_powershell:
        script: |
          $ErrorActionPreference = 'Stop'
          $toolkitDir = "{{ aw_windows_deploy_root }}\windows"
          $encIn = New-Object System.Text.UTF8Encoding($false)
          $encOut = New-Object System.Text.UTF8Encoding($true)
          Get-ChildItem -LiteralPath $toolkitDir -File -Include *.ps1,*.psm1,*.psd1 | ForEach-Object {
            $text = [System.IO.File]::ReadAllText($_.FullName, $encIn)
            [System.IO.File]::WriteAllText($_.FullName, $text, $encOut)
          }

    - name: Загрузить список пользователей для доменного развёртывания
      ansible.windows.win_copy:
        dest: "{{ aw_windows_deploy_root }}\\windows\\users.txt"
        content: |
          {% for user in aw_windows_users_effective -%}
          {{ user }}
          {% endfor -%}

    - name: Проверить нужен ли migration с legacy ActivityWatch путей
      when: aw_windows_migration_enabled | bool
      ansible.windows.win_stat:
        path: "{{ aw_windows_legacy_state_root }}\\deployment-config.json"
      register: aw_windows_legacy_config

    - name: Выполнить безопасную migration legacy prod в AWatch-rus
      when:
        - aw_windows_migration_enabled | bool
        - aw_windows_legacy_config.stat.exists | default(false)
      ansible.windows.win_powershell:
        script: |
          $ErrorActionPreference = 'Stop'
          $result = & "{{ aw_windows_deploy_root }}\windows\migrate-awatch-rus-paths.ps1" `
            -OldInstallRoot "{{ aw_windows_legacy_install_root }}" `
            -OldStateRoot "{{ aw_windows_legacy_state_root }}" `
            -NewInstallRoot "{{ aw_windows_install_root }}" `
            -NewStateRoot "{{ aw_windows_state_root }}" `
            -ToolkitRoot "{{ aw_windows_deploy_root }}\windows"
          $result | ConvertTo-Json -Depth 8 | Out-File -FilePath "{{ aw_windows_migration_report_remote_path }}" -Encoding utf8

    - name: Запустить Windows/RDP ensemble развёртывание
      ansible.windows.win_powershell:
        script: |
          $ErrorActionPreference = 'Stop'
          $env:AWATCH_RUS_BUILTIN_ADMINISTRATOR_NAME = "{{ aw_windows_builtin_administrator_name }}"
          $params = @{
            ServerScheme = "{{ aw_windows_server_scheme }}"
            ServerHost = "{{ aw_windows_server_host_effective }}"
            ServerPort = {{ aw_windows_server_port }}
            Version = "{{ aw_windows_package_version }}"
            Domain = "{{ aw_windows_domain }}"
            UserListPath = "{{ aw_windows_deploy_root }}\windows\users.txt"
            InstallRoot = "{{ aw_windows_install_root }}"
            StateRoot = "{{ aw_windows_state_root }}"
            AfkEnabled = {{ '$true' if (aw_windows_afk_enabled_effective | bool) else '$false' }}
            WindowEnabled = {{ '$true' if (aw_windows_window_enabled_effective | bool) else '$false' }}
            FileOpsEnabled = {{ '$true' if (aw_windows_file_ops_enabled | bool) else '$false' }}
            LocalAgentLogsEnabled = {{ '$true' if (aw_windows_local_agent_logs_enabled | bool) else '$false' }}
            IncidentCaptureEnabled = {{ '$true' if (aw_windows_incident_capture_enabled | bool) else '$false' }}
            IncidentScreenshotEnabled = {{ '$true' if (aw_windows_incident_screenshot_enabled | bool) else '$false' }}
            IncidentArtifactsRoot = "{{ aw_windows_incident_artifacts_root }}"
            EvtxExportRoot = "{{ aw_windows_forensics_root }}"
            EvtxRetentionDays = {{ aw_windows_evtx_retention_days | int }}
            LogonMarkerEnabled = {{ '$true' if (aw_windows_logon_marker_enabled | bool) else '$false' }}
            ProcessEventsEnabled = {{ '$true' if (aw_windows_process_events_enabled | bool) else '$false' }}
            PolicyMode = "{{ aw_windows_policy_mode }}"
            PolicyEngineEnabled = {{ '$true' if (aw_windows_policy_engine_enabled | bool) else '$false' }}
            PolicyEngineHost = "{{ aw_windows_policy_engine_host_effective }}"
            PolicyEnginePort = {{ aw_windows_policy_engine_port }}
            PolicyEngineScheme = "{{ aw_windows_policy_engine_scheme }}"
            PolicyRefreshSeconds = {{ aw_windows_policy_refresh_seconds }}
            HayabusaAutoUploadEnabled = {{ '$true' if (aw_windows_hayabusa_auto_upload_enabled | bool) else '$false' }}
            HayabusaAutoUploadIntervalHours = {{ aw_windows_hayabusa_auto_upload_interval_hours | int }}
            HayabusaAutoUploadHoursBack = {{ aw_windows_hayabusa_auto_upload_hours_back | int }}
            HayabusaAutoUploadMode = "{{ aw_windows_hayabusa_auto_upload_mode }}"
            HayabusaAutoUploadTaskName = "{{ aw_windows_hayabusa_auto_upload_task_name }}"
            HayabusaAutoUploadRunAsUser = "{{ aw_windows_hayabusa_auto_upload_run_as_user }}"
            File1CAutoUploadEnabled = {{ '$true' if (aw_windows_file_1c_auto_upload_enabled | bool) else '$false' }}
            File1CAutoUploadIntervalMinutes = {{ aw_windows_file_1c_auto_upload_interval_minutes | int }}
            File1CAutoUploadTaskName = "{{ aw_windows_file_1c_auto_upload_task_name }}"
            File1CAutoUploadRunAsUser = "{{ aw_windows_file_1c_auto_upload_run_as_user }}"
            File1CTargetHost = "{{ aw_windows_file_1c_target_host_effective }}"
            File1CTargetUser = "{{ aw_windows_file_1c_target_user }}"
            File1CRegistryWorkbookPath = "{{ aw_windows_file_1c_registry_workbook_path }}"
            CustomRulesPath = "{{ aw_windows_rules_path }}"
            CustomPolicyPath = "{{ aw_windows_policy_path }}"
          }
          {% if (aw_windows_package_url | default('') | string | length) > 0 %}
          $params.PackageUrl = "{{ aw_windows_package_url }}"
          {% endif %}
          {% if (aw_windows_package_zip_path | default('') | string | length) > 0 %}
          $params.PackageZipPath = "{{ aw_windows_package_zip_path }}"
          {% endif %}
          {% if (aw_windows_evtx_channels | default([]) | length) > 0 %}
          $params.EvtxChannels = @(
          {% for channel in aw_windows_evtx_channels %}
            "{{ channel }}"{% if not loop.last %},{% endif %}
          {% endfor %}
          )
          {% endif %}
          {% if (aw_windows_hostname_override | default('') | string | length) > 0 %}
          $params.AwHostname = "{{ aw_windows_hostname_override }}"
          {% endif %}
          {% if aw_windows_skip_hardening | bool %}
          $params.SkipHardening = $true
          {% endif %}
          {% if aw_windows_integration_test_enabled | bool %}
          $params.IntegrationTestEnabled = $true
          {% endif %}
          & "{{ aw_windows_deploy_root }}\windows\deploy-ensemble.ps1" @params

    - name: Удалить лишние ActivityWatch Launch tasks вне текущего deployment-config
      ansible.windows.win_powershell:
        script: |
          $ErrorActionPreference = 'Stop'
          $config = Get-Content -Raw -LiteralPath "{{ aw_windows_state_root }}\deployment-config.json" | ConvertFrom-Json
          $desired = @($config.userTasks | ForEach-Object { [string]$_.LaunchTaskName })
          foreach ($task in @(Get-ScheduledTask | Where-Object { $_.TaskName -like 'ActivityWatch Launch *' })) {
            if ($desired -notcontains [string]$task.TaskName) {
              Unregister-ScheduledTask -TaskName $task.TaskName -Confirm:$false -ErrorAction SilentlyContinue
              & cmd.exe /c "schtasks /Delete /TN `"$($task.TaskName)`" /F >nul 2>&1" | Out-Null
            }
          }

    - name: Принудительно запустить ActivityWatch recovery и launch tasks
      when: aw_windows_force_task_restart | bool
      ansible.windows.win_powershell:
        script: |
          $ErrorActionPreference = 'Stop'

          function Get-CollectorKey {
            param([string]$CommandLine)
            if (-not $CommandLine) { return $null }
            $cl = $CommandLine.ToLowerInvariant()
            if ($cl -like '*browser-domains-native-collector.ps1*') { return 'browser' }
            if ($cl -like '*file-operations-collector.ps1*') { return 'fileops' }
            if ($cl -like '*dlp-endpoint-signals-collector.ps1*') { return 'endpoint' }
            if ($cl -like '*email-outbound-collector.ps1*') { return 'email' }
            if ($cl -like '*worktime-session-collector.ps1*') { return 'worktime' }
            return $null
          }

          $collectorProcs = Get-CimInstance Win32_Process |
            Where-Object { $_.Name -eq 'powershell.exe' -and $_.CommandLine } |
            ForEach-Object {
              $key = Get-CollectorKey -CommandLine $_.CommandLine
              if ($key) {
                $groupKey = if ($key -eq 'worktime') { 'worktime::global' } else { '{0}::{1}' -f $key, ([int]$_.SessionId) }
                [pscustomobject]@{
                  ProcessId    = [int]$_.ProcessId
                  SessionId    = [int]$_.SessionId
                  CreationDate = $_.CreationDate
                  CollectorKey = $key
                  GroupKey     = $groupKey
                }
              }
            } |
            Where-Object { $_ -ne $null }

          # Keep only one process per collector scope: worktime collector is global, others stay per-session.
          foreach ($group in ($collectorProcs | Group-Object GroupKey)) {
            $ordered = @($group.Group | Sort-Object CreationDate -Descending)
            if ($ordered.Count -le 1) { continue }
            foreach ($dup in $ordered | Select-Object -Skip 1) {
              Stop-Process -Id $dup.ProcessId -Force -ErrorAction SilentlyContinue
            }
          }

          # Force managed collectors/watchers to reload the freshly deployed scripts.
          Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
            Where-Object {
              (
                ($_.Name -ieq 'powershell.exe' -or $_.Name -ieq 'pwsh.exe') -and
                $_.CommandLine -match 'C:\\ProgramData\\AWatch-rus\\' -and
                $_.CommandLine -match '(collector|launch-watchers|recovery-loop)\.ps1'
              ) -or
              ($_.Name -ieq 'aw-watcher-afk.exe') -or
              ($_.Name -ieq 'aw-watcher-window.exe')
            } |
            ForEach-Object {
              Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue
            }

          Start-Sleep -Seconds 2

          $modulePath = "{{ aw_windows_deploy_root }}\windows\ActivityWatch.Windows.Common.psm1"
          Import-Module $modulePath -Force

          # Ensure tasks are enabled (some environments keep them disabled, causing "0s" in WebUI).
          try {
            Enable-ScheduledTask -TaskName "{{ aw_windows_recovery_task_name }}" -ErrorAction SilentlyContinue | Out-Null
          } catch {}

          $config = Get-Content -Raw -LiteralPath "{{ aw_windows_state_root }}\deployment-config.json" | ConvertFrom-Json
          $configPaths = Get-ActivityWatchRecoveryConfigPaths -PrimaryConfigPath "{{ aw_windows_state_root }}\deployment-config.json"
          $taskDefs = @(Get-ActivityWatchRecoveryTaskDefinitions -ConfigPaths $configPaths)
          $sessionRecords = @(Get-ActivityWatchSessionRecords)

          foreach ($taskDef in @($taskDefs)) {
            try { Enable-ScheduledTask -TaskName ([string]$taskDef.taskName) -ErrorAction SilentlyContinue | Out-Null } catch {}
          }

          $recoveryTask = Get-ScheduledTask -TaskName "{{ aw_windows_recovery_task_name }}" -ErrorAction SilentlyContinue
          if ($recoveryTask -and $recoveryTask.State -notin @('Running', 'Queued')) {
            Start-ScheduledTask -TaskName "{{ aw_windows_recovery_task_name }}"
          }
          foreach ($taskDef in @($taskDefs)) {
            if (-not (Test-ActivityWatchUserHasManagedSession -UserId ([string]$taskDef.userId) -SessionRecords $sessionRecords -IncludeLive -IncludeDisconnected)) {
              continue
            }
            $launchTask = Get-ScheduledTask -TaskName ([string]$taskDef.taskName) -ErrorAction SilentlyContinue
            if ($launchTask -and $launchTask.State -notin @('Running', 'Queued')) {
              Start-ScheduledTask -TaskName ([string]$taskDef.taskName) -ErrorAction SilentlyContinue
            }
          }

    - name: Установить session-aware AWatch-rus Collector Guard service
      when: aw_windows_collector_guard_enabled | bool
      ansible.windows.win_powershell:
        script: |
          $ErrorActionPreference = 'Stop'
          $guardParams = @{
            ConfigPath = "{{ aw_windows_state_root }}\deployment-config.json"
            Mode = "{{ aw_windows_collector_guard_mode }}"
            ServiceName = "{{ aw_windows_collector_guard_service_name }}"
            LoopSeconds = {{ aw_windows_collector_guard_loop_seconds | int }}
          }
          & "{{ aw_windows_deploy_root }}\windows\install-collector-guard-service.ps1" @guardParams

    - name: Получить Windows hostname для AW smoke-check bucket
      when:
        - aw_windows_api_smoke_check_enabled | bool
      ansible.windows.win_command: powershell.exe -NoProfile -Command "$env:COMPUTERNAME"
      register: aw_windows_hostname_result
      changed_when: false

    - name: Вычислить AW worktime smoke-check bucket
      when:
        - aw_windows_api_smoke_check_enabled | bool
        - aw_windows_hostname_result.stdout is defined
      ansible.builtin.set_fact:
        aw_windows_api_smoke_check_bucket_effective: >-
          {{
            aw_windows_api_smoke_check_bucket
            if (aw_windows_api_smoke_check_bucket | default('') | string | length) > 0
            else 'aw-worktime-sessions_' ~ (aw_windows_hostname_result.stdout | trim)
          }}

    - name: Вычислить AW Window smoke-check bucket
      when:
        - aw_windows_api_smoke_check_enabled | bool
        - aw_windows_api_smoke_check_window_enabled_effective | bool
        - aw_windows_window_enabled_effective | bool
        - aw_windows_hostname_result.stdout is defined
      ansible.builtin.set_fact:
        aw_windows_api_smoke_check_window_bucket_effective: >-
          {{
            aw_windows_api_smoke_check_window_bucket
            if (aw_windows_api_smoke_check_window_bucket | default('') | string | length) > 0
            else 'aw-watcher-window_' ~ (aw_windows_hostname_result.stdout | trim)
          }}

    - name: Выполнить AW API smoke-check (worktime bucket должен получать события)
      when:
        - aw_windows_api_smoke_check_enabled | bool
      ansible.builtin.uri:
        url: "{{ aw_windows_server_scheme }}://{{ aw_windows_server_host_effective }}:{{ aw_windows_server_port }}/api/0/buckets/{{ aw_windows_api_smoke_check_bucket_effective }}/events?limit={{ aw_windows_api_smoke_check_limit }}"
        method: GET
        status_code: 200
        return_content: true
      register: aw_windows_api_smoke_result
      failed_when: false
      until: >-
        (aw_windows_api_smoke_result.status | default(0)) == 200
        and ((aw_windows_api_smoke_result.json | default([])) | length) >= aw_windows_api_smoke_check_min_events
        and (
          (aw_windows_api_smoke_result.json | default([]))
          | selectattr('data.source', 'equalto', 'worktime-session-collector')
          | list
          | length
        ) > 0
      retries: 12
      delay: 5
      ignore_errors: "{{ (not aw_windows_fail_on_validation_error | bool) }}"
      delegate_to: localhost
      changed_when: false

    - name: Выполнить AW API smoke-check (Window bucket должен получать события)
      when:
        - aw_windows_api_smoke_check_enabled | bool
        - aw_windows_api_smoke_check_window_enabled_effective | bool
        - aw_windows_window_enabled_effective | bool
      ansible.builtin.uri:
        url: "{{ aw_windows_server_scheme }}://{{ aw_windows_server_host_effective }}:{{ aw_windows_server_port }}/api/0/buckets/{{ aw_windows_api_smoke_check_window_bucket_effective }}/events?limit={{ aw_windows_api_smoke_check_limit }}"
        method: GET
        status_code: 200
        return_content: true
      register: aw_windows_api_smoke_window_result
      failed_when: false
      until: >-
        (aw_windows_api_smoke_window_result.status | default(0)) == 200
        and ((aw_windows_api_smoke_window_result.json | default([])) | length) >= aw_windows_api_smoke_check_min_events
      retries: 12
      delay: 5
      ignore_errors: "{{ (not aw_windows_fail_on_validation_error | bool) }}"
      delegate_to: localhost
      changed_when: false

    - name: Валидировать развёртывание на эндпоинте
      ansible.windows.win_powershell:
        script: |
          $ErrorActionPreference = 'Stop'
          $result = & "{{ aw_windows_deploy_root }}\windows\validate-deployment.ps1" `
            -ConfigPath "{{ aw_windows_state_root }}\deployment-config.json"
          $result | ConvertTo-Json -Depth 12 | Out-File -FilePath "{{ aw_windows_validation_remote_path }}" -Encoding utf8
          return $result

    - name: Создать локальную директорию для отчётов валидации
      ansible.builtin.file:
        path: "{{ aw_windows_validation_local_dir }}"
        state: directory
        mode: "0755"
      delegate_to: localhost

    - name: Стянуть отчёт валидации с эндпоинта
      ansible.builtin.fetch:
        src: "{{ aw_windows_validation_remote_path }}"
        dest: "{{ aw_windows_validation_local_dir }}/{{ inventory_hostname }}-aw_validate_ansible.json"
        flat: true

    - name: Убедиться, что каталог Windows validation существует на AW server
      when:
        - aw_server_inventory_host_effective | length > 0
        - aw_windows_health_validation_dir_effective | length > 0
      ansible.builtin.file:
        path: "{{ aw_windows_health_validation_dir_effective }}"
        state: directory
        mode: "0755"
      delegate_to: "{{ aw_server_inventory_host_effective }}"

    - name: Опубликовать validation report на AW server для aw-rus-healthd
      when:
        - aw_server_inventory_host_effective | length > 0
        - aw_windows_health_validation_dir_effective | length > 0
      ansible.builtin.copy:
        src: "{{ aw_windows_validation_local_dir }}/{{ inventory_hostname }}-aw_validate_ansible.json"
        dest: "{{ aw_windows_health_validation_dir_effective }}/{{ inventory_hostname }}-aw_validate_ansible.json"
        mode: "0644"
      delegate_to: "{{ aw_server_inventory_host_effective }}"

    - name: Проверить статус валидации
      ansible.builtin.shell: |
        python3 - <<'PY'
        import json, sys
        with open('{{ aw_windows_validation_local_dir }}/{{ inventory_hostname }}-aw_validate_ansible.json', 'r', encoding='utf-8-sig') as f:
            data = json.load(f)
        if not data.get('overallOk', False):
            failed = ", ".join(data.get("summary", {}).get("failedSections", [])) or "unknown"
            print(f"Validation failed for {{ inventory_hostname }}: {failed}")
            sys.exit(1)
        PY
      delegate_to: localhost
      when: aw_windows_fail_on_validation_error | bool
