---
# SSH Mesh Key Distribution & Verification
#
# Distributes SSH public keys across all managed hosts so every host can SSH
# to every other host. Also verifies connectivity.
#
# Usage:
#   ansible-playbook -i inventory.yml playbooks/ssh_mesh.yml
#   ansible-playbook -i inventory.yml playbooks/ssh_mesh.yml --tags verify
#   ansible-playbook -i inventory.yml playbooks/ssh_mesh.yml --tags distribute
#   ansible-playbook -i inventory.yml playbooks/ssh_mesh.yml -e "generate_missing=true"

- name: SSH Mesh — Collect Keys
  hosts: ssh_mesh
  gather_facts: false
  tags: [collect, distribute]

  tasks:
    - name: Check if ed25519 key exists
      stat:
        path: "~/.ssh/id_ed25519.pub"
      register: ed25519_key

    - name: Check if RSA key exists (fallback)
      stat:
        path: "~/.ssh/id_rsa.pub"
      register: rsa_key
      when: not ed25519_key.stat.exists

    - name: Generate ed25519 key if missing
      command: ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -N "" -C "{{ ansible_user }}@{{ inventory_hostname }}"
      args:
        creates: ~/.ssh/id_ed25519
      when:
        - not ed25519_key.stat.exists
        - not (rsa_key.stat.exists | default(false))
        - generate_missing | default(false) | bool

    - name: Re-check for ed25519 key after generation
      stat:
        path: "~/.ssh/id_ed25519.pub"
      register: ed25519_key_recheck
      when:
        - not ed25519_key.stat.exists
        - generate_missing | default(false) | bool

    - name: Read ed25519 public key
      slurp:
        src: "~/.ssh/id_ed25519.pub"
      register: pubkey_ed25519
      when: ed25519_key.stat.exists or (ed25519_key_recheck.stat.exists | default(false))

    - name: Read RSA public key (fallback)
      slurp:
        src: "~/.ssh/id_rsa.pub"
      register: pubkey_rsa
      when:
        - not ed25519_key.stat.exists
        - not (ed25519_key_recheck.stat.exists | default(false))
        - rsa_key.stat.exists | default(false)

    - name: Set public key fact
      set_fact:
        ssh_pubkey: >-
          {{ (pubkey_ed25519.content | default(pubkey_rsa.content) | b64decode | trim) }}
        ssh_key_comment: "{{ inventory_hostname }}"
      when: pubkey_ed25519 is not skipped or pubkey_rsa is not skipped

    - name: Warn if no key found
      debug:
        msg: "WARNING: No SSH key on {{ inventory_hostname }}. Run with -e generate_missing=true to create one."
      when: ssh_pubkey is not defined

- name: SSH Mesh — Distribute Keys
  hosts: ssh_mesh
  gather_facts: false
  tags: [distribute]

  tasks:
    - name: Build list of all mesh public keys
      set_fact:
        all_mesh_keys: >-
          {{ groups['ssh_mesh']
             | map('extract', hostvars)
             | selectattr('ssh_pubkey', 'defined')
             | map(attribute='ssh_pubkey')
             | list }}

    - name: Include admin key
      set_fact:
        all_mesh_keys: >-
          {{ all_mesh_keys + [admin_key] }}
      when: admin_key is defined

    - name: Ensure .ssh directory exists
      file:
        path: "~/.ssh"
        state: directory
        mode: "0700"

    - name: Ensure authorized_keys exists
      file:
        path: "~/.ssh/authorized_keys"
        state: touch
        mode: "0600"
      changed_when: false

    - name: Add missing keys to authorized_keys
      lineinfile:
        path: "~/.ssh/authorized_keys"
        line: "{{ item }}"
        state: present
      loop: "{{ all_mesh_keys }}"
      loop_control:
        label: "{{ item.split()[-1] | default('unknown') }}"

- name: SSH Mesh — Verify Connectivity
  hosts: localhost
  gather_facts: false
  connection: local
  tags: [verify]

  tasks:
    - name: Build mesh host list
      set_fact:
        mesh_hosts: >-
          {{ groups['ssh_mesh']
             | map('extract', hostvars)
             | list }}

    - name: Test SSH from localhost to each mesh host
      shell: |
        ssh -o BatchMode=yes \
            -o ConnectTimeout=5 \
            -o StrictHostKeyChecking=accept-new \
            -i ~/.ssh/id_ed25519 \
            -p {{ item.ansible_port | default(22) }} \
            {{ item.ansible_user }}@{{ item.ansible_host }} \
            "echo ok" 2>&1
      register: ssh_tests
      loop: "{{ mesh_hosts }}"
      loop_control:
        label: "localhost -> {{ item.inventory_hostname | default(item.ansible_host) }}"
      failed_when: false
      changed_when: false

    - name: Display connectivity matrix
      debug:
        msg: |
          SSH Mesh Verification (from localhost):
          {% for result in ssh_tests.results %}
          {{ '✓' if result.rc == 0 and 'ok' in (result.stdout | default('')) else '✗' }} -> {{ result.item.inventory_hostname | default(result.item.ansible_host) }}{% if result.rc != 0 or 'ok' not in (result.stdout | default('')) %} ({{ result.stdout_lines[-1] | default('unknown error') }}){% endif %}

          {% endfor %}
          {{ ssh_tests.results | selectattr('rc', 'equalto', 0) | list | length }}/{{ ssh_tests.results | length }} hosts reachable

    - name: Test cross-host SSH (sample pairs)
      shell: |
        results=""
        {% for pair in cross_test_pairs | default([]) %}
        src_user="{{ pair.src_user }}"
        src_host="{{ pair.src_host }}"
        src_port="{{ pair.src_port | default(22) }}"
        dst_user="{{ pair.dst_user }}"
        dst_host="{{ pair.dst_host }}"
        dst_port="{{ pair.dst_port | default(22) }}"
        out=$(ssh -o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=no \
              -p ${src_port} ${src_user}@${src_host} \
              "ssh -o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=accept-new \
                   -i ~/.ssh/id_ed25519 -p ${dst_port} ${dst_user}@${dst_host} 'echo ok'" 2>&1)
        if echo "$out" | grep -q "ok"; then
          results="${results}✓ {{ pair.label }}\n"
        else
          results="${results}✗ {{ pair.label }} ($(echo "$out" | tail -1))\n"
        fi
        {% endfor %}
        echo -e "$results"
      register: cross_tests
      when: cross_test_pairs is defined
      changed_when: false

    - name: Display cross-host results
      debug:
        msg: |
          Cross-Host SSH Tests:
          {{ cross_tests.stdout }}
      when: cross_tests is not skipped and cross_tests.stdout is defined
