---
# JellyShip System Playbook — Idempotent State Management
# Run: ansible-playbook /opt/jellyship/ansible/site.yml
# All tasks are idempotent — safe to re-run anytime.

- name: JellyShip System Configuration
  hosts: localhost
  connection: local
  become: true
  vars:
    jellyship_user: jellyship
    jellyship_pass: jellyship
    jellyship_group: jellyship-users
    jellyship_repo: https://forgejo.brads.computer/brad/jellyship
    jellyship_dir: /opt/jellyship
    media_dir: /srv/jellyship
    jellyfin_url: http://127.0.0.1:8096
    jellyship_port: 3000
    credential_state: /etc/jellyship/.credential-sync-state.json

  tasks:
    # ── Phase 1: Core System ──────────────────────────────────────────
    - name: "Phase 1 | Create jellyship group"
      ansible.builtin.group:
        name: "{{ jellyship_group }}"
        state: present

    - name: "Phase 1 | Create jellyship user"
      ansible.builtin.user:
        name: "{{ jellyship_user }}"
        groups: "{{ jellyship_group }}"
        append: true
        shell: /bin/bash
        create_home: true

    - name: "Phase 1 | Create config directory"
      ansible.builtin.file:
        path: /etc/jellyship
        state: directory
        mode: '0755'

    - name: "Phase 1 | Create media directories"
      ansible.builtin.file:
        path: "{{ media_dir }}/{{ item }}"
        state: directory
        owner: "{{ jellyship_user }}"
        group: "{{ jellyship_group }}"
        mode: '2775'
      loop:
        - movies
        - shows
        - music
        - downloads

    # ── Phase 1: Node.js ──────────────────────────────────────────────
    - name: "Phase 1 | Check Node.js installed"
      ansible.builtin.command: node --version
      register: node_check
      changed_when: false
      failed_when: false

    - name: "Phase 1 | Install Node.js 22 repo"
      ansible.builtin.shell: curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
      when: node_check.rc != 0
      args:
        creates: /etc/apt/sources.list.d/nodesource.list

    - name: "Phase 1 | Install Node.js"
      ansible.builtin.apt:
        name: nodejs
        state: present
        update_cache: true
      when: node_check.rc != 0

    # ── Phase 1: Jellyfin ─────────────────────────────────────────────
    - name: "Phase 1 | Check Jellyfin installed"
      ansible.builtin.command: dpkg-query -W -f='${Status}' jellyfin-server
      register: jf_check
      changed_when: false
      failed_when: false

    - name: "Phase 1 | Resize /tmp for Jellyfin install"
      ansible.builtin.command: mount -o remount,size=3G /tmp
      when: jf_check.rc != 0
      changed_when: false
      failed_when: false

    - name: "Phase 1 | Install Jellyfin"
      ansible.builtin.shell: curl -fsSL https://repo.jellyfin.org/install-debuntu.sh | bash
      when: jf_check.rc != 0
      args:
        creates: /usr/bin/jellyfin

    - name: "Phase 1 | Ensure Jellyfin running"
      ansible.builtin.systemd:
        name: jellyfin
        state: started
        enabled: true

    - name: "Phase 1 | Add jellyfin user to jellyship-users group"
      ansible.builtin.user:
        name: jellyfin
        groups: "{{ jellyship_group }}"
        append: true

    # ── Phase 1: JellyShip App ────────────────────────────────────────
    - name: "Phase 1 | Check if repo already cloned"
      ansible.builtin.stat:
        path: "{{ jellyship_dir }}/.git"
      register: git_dir

    - name: "Phase 1 | Clone JellyShip repo"
      ansible.builtin.shell: |
        cd {{ jellyship_dir }}
        git init
        git remote add origin {{ jellyship_repo }} 2>/dev/null || git remote set-url origin {{ jellyship_repo }}
        git fetch origin
        git checkout -f main || git checkout -f -b main origin/main
      environment:
        HOME: /root
      when: not git_dir.stat.exists
      register: git_clone
      failed_when: false

    - name: "Phase 1 | Configure git safe directory"
      ansible.builtin.command: git config --global --add safe.directory {{ jellyship_dir }}
      environment:
        HOME: /root
      changed_when: false

    - name: "Phase 1 | Pull latest JellyShip"
      ansible.builtin.command: git pull --ff-only
      args:
        chdir: "{{ jellyship_dir }}"
      environment:
        HOME: /root
      when: git_dir.stat.exists
      register: git_pull
      changed_when: "'Already up to date' not in (git_pull.stdout | default(''))"
      failed_when: false

    - name: "Phase 1 | Set git result"
      ansible.builtin.set_fact:
        jellyship_code_changed: "{{ (git_clone is defined and git_clone.changed) or (git_pull is defined and git_pull.changed) }}"

    - name: "Phase 1 | Install npm dependencies"
      ansible.builtin.command: npm install
      args:
        chdir: "{{ jellyship_dir }}"
      when: jellyship_code_changed | bool
      changed_when: jellyship_code_changed | bool
      failed_when: false

    - name: "Phase 1 | Build JellyShip"
      ansible.builtin.command: npm run build
      args:
        chdir: "{{ jellyship_dir }}"
      when: jellyship_code_changed | bool
      changed_when: jellyship_code_changed | bool
      failed_when: false

    - name: "Phase 1 | Create JellyShip data dir"
      ansible.builtin.file:
        path: /var/lib/jellyship
        state: directory
        owner: "{{ jellyship_user }}"
        group: "{{ jellyship_user }}"
        mode: '0755'

    - name: "Phase 1 | Set JellyShip ownership"
      ansible.builtin.file:
        path: "{{ jellyship_dir }}"
        state: directory
        owner: "{{ jellyship_user }}"
        group: "{{ jellyship_user }}"
        recurse: true

    - name: "Phase 1 | Install JellyShip service"
      ansible.builtin.copy:
        dest: /etc/systemd/system/jellyship.service
        content: |
          [Unit]
          Description=JellyShip Content Manager
          After=network-online.target
          Wants=network-online.target

          [Service]
          Type=simple
          User=jellyship
          WorkingDirectory=/opt/jellyship
          ExecStart=/usr/bin/node dist/index.js --host 127.0.0.1 --auth none
          Restart=on-failure
          RestartSec=5
          Environment=NODE_ENV=production

          [Install]
          WantedBy=multi-user.target
        mode: '0644'
      notify: reload systemd

    - name: "Phase 1 | Ensure JellyShip running"
      ansible.builtin.systemd:
        name: jellyship
        state: started
        enabled: true

    # ── Phase 1: Auth Proxy ───────────────────────────────────────────
    - name: "Phase 1 | Install auth proxy script"
      ansible.builtin.copy:
        src: "{{ jellyship_dir }}/jellyfin-auth-proxy.py"
        dest: /opt/jellyship/jellyfin-auth-proxy.py
        mode: '0755'
        remote_src: true
      failed_when: false

    - name: "Phase 1 | Install auth proxy service"
      ansible.builtin.copy:
        dest: /etc/systemd/system/jellyship-auth.service
        content: |
          [Unit]
          Description=JellyShip Jellyfin Auth Proxy
          After=network-online.target jellyfin.service
          Wants=network-online.target

          [Service]
          Type=simple
          ExecStart=/usr/bin/python3 /opt/jellyship/jellyfin-auth-proxy.py
          Restart=on-failure
          RestartSec=5

          [Install]
          WantedBy=multi-user.target
        mode: '0644'
      notify: reload systemd

    - name: "Phase 1 | Ensure auth proxy running"
      ansible.builtin.systemd:
        name: jellyship-auth
        state: started
        enabled: true

    # ── Phase 1: nginx ────────────────────────────────────────────────
    - name: "Phase 1 | Install nginx"
      ansible.builtin.apt:
        name: nginx
        state: present

    - name: "Phase 1 | Install nginx config"
      ansible.builtin.copy:
        src: "{{ jellyship_dir }}/jellyship-nginx.conf"
        dest: /etc/nginx/sites-available/jellyship
        remote_src: true
        mode: '0644'
      notify: reload nginx
      failed_when: false

    - name: "Phase 1 | Enable nginx site"
      ansible.builtin.file:
        src: /etc/nginx/sites-available/jellyship
        dest: /etc/nginx/sites-enabled/jellyship
        state: link
      notify: reload nginx
      failed_when: false

    - name: "Phase 1 | Remove default nginx site"
      ansible.builtin.file:
        path: /etc/nginx/sites-enabled/default
        state: absent
      notify: reload nginx

    - name: "Phase 1 | Ensure nginx running"
      ansible.builtin.systemd:
        name: nginx
        state: started
        enabled: true

    # ── Jellyfin Auto-Configuration ───────────────────────────────────
    - name: "Jellyfin | Wait for API"
      ansible.builtin.uri:
        url: "{{ jellyfin_url }}/System/Info/Public"
        method: GET
        return_content: true
      register: jf_info
      until: jf_info.status == 200
      retries: 30
      delay: 2

    - name: "Jellyfin | Check wizard status"
      ansible.builtin.set_fact:
        wizard_done: "{{ (jf_info.json.StartupWizardCompleted | default(false)) | bool }}"

    - name: "Jellyfin | Set initial configuration"
      ansible.builtin.uri:
        url: "{{ jellyfin_url }}/Startup/Configuration"
        method: POST
        body_format: json
        body:
          UICulture: en-US
          MetadataCountryCode: US
          PreferredMetadataLanguage: en
        status_code: [200, 204, 404]
      when: not wizard_done

    - name: "Jellyfin | Create admin user"
      ansible.builtin.uri:
        url: "{{ jellyfin_url }}/Startup/User"
        method: POST
        body_format: json
        body:
          Name: "{{ jellyship_user }}"
          Password: "{{ jellyship_pass }}"
        status_code: [200, 204, 404]
      when: not wizard_done

    - name: "Jellyfin | Enable remote access"
      ansible.builtin.uri:
        url: "{{ jellyfin_url }}/Startup/RemoteAccess"
        method: POST
        body_format: json
        body:
          EnableRemoteAccess: true
          EnableAutomaticPortMapping: false
        status_code: [200, 204, 404]
      when: not wizard_done

    - name: "Jellyfin | Complete wizard"
      ansible.builtin.uri:
        url: "{{ jellyfin_url }}/Startup/Complete"
        method: POST
        status_code: [200, 204, 404]
      when: not wizard_done

    - name: "Jellyfin | Authenticate as admin"
      ansible.builtin.uri:
        url: "{{ jellyfin_url }}/Users/AuthenticateByName"
        method: POST
        body_format: json
        headers:
          X-Emby-Authorization: 'MediaBrowser Client="JellyShip", Device="Server", DeviceId="jellyship-ansible", Version="1.0.0"'
        body:
          Username: "{{ jellyship_user }}"
          Pw: "{{ jellyship_pass }}"
        return_content: true
      register: jf_auth

    - name: "Jellyfin | Set auth token"
      ansible.builtin.set_fact:
        jf_token: "{{ jf_auth.json.AccessToken }}"

    - name: "Jellyfin | Check existing libraries"
      ansible.builtin.uri:
        url: "{{ jellyfin_url }}/Library/VirtualFolders"
        headers:
          X-MediaBrowser-Token: "{{ jf_token }}"
        return_content: true
      register: jf_libs

    - name: "Jellyfin | Add Movies library"
      ansible.builtin.uri:
        url: "{{ jellyfin_url }}/Library/VirtualFolders?collectionType=movies&refreshLibrary=false&name=Movies"
        method: POST
        headers:
          X-MediaBrowser-Token: "{{ jf_token }}"
        body_format: json
        body:
          LibraryOptions:
            EnableRealtimeMonitor: true
            EnableChapterImageExtraction: false
            SaveLocalMetadata: true
            EnableInternetProviders: true
            AutomaticRefreshIntervalDays: 30
            PathInfos:
              - Path: "{{ media_dir }}/movies"
        status_code: [200, 204]
      when: jf_libs.json | selectattr('Name', 'eq', 'Movies') | list | length == 0

    - name: "Jellyfin | Add Shows library"
      ansible.builtin.uri:
        url: "{{ jellyfin_url }}/Library/VirtualFolders?collectionType=tvshows&refreshLibrary=false&name=Shows"
        method: POST
        headers:
          X-MediaBrowser-Token: "{{ jf_token }}"
        body_format: json
        body:
          LibraryOptions:
            EnableRealtimeMonitor: true
            EnableChapterImageExtraction: false
            SaveLocalMetadata: true
            EnableInternetProviders: true
            AutomaticRefreshIntervalDays: 30
            PathInfos:
              - Path: "{{ media_dir }}/shows"
        status_code: [200, 204]
      when: jf_libs.json | selectattr('Name', 'eq', 'Shows') | list | length == 0

    - name: "Jellyfin | Add Music library"
      ansible.builtin.uri:
        url: "{{ jellyfin_url }}/Library/VirtualFolders?collectionType=music&refreshLibrary=false&name=Music"
        method: POST
        headers:
          X-MediaBrowser-Token: "{{ jf_token }}"
        body_format: json
        body:
          LibraryOptions:
            EnableRealtimeMonitor: true
            EnableChapterImageExtraction: false
            SaveLocalMetadata: true
            EnableInternetProviders: true
            AutomaticRefreshIntervalDays: 30
            PathInfos:
              - Path: "{{ media_dir }}/music"
        status_code: [200, 204]
      when: jf_libs.json | selectattr('Name', 'eq', 'Music') | list | length == 0

    - name: "Jellyfin | Check existing API keys"
      ansible.builtin.uri:
        url: "{{ jellyfin_url }}/Auth/Keys"
        headers:
          X-MediaBrowser-Token: "{{ jf_token }}"
        return_content: true
      register: jf_keys

    - name: "Jellyfin | Create API key for credential sync"
      ansible.builtin.uri:
        url: "{{ jellyfin_url }}/Auth/Keys?app=JellyShip+Credential+Sync"
        method: POST
        headers:
          X-MediaBrowser-Token: "{{ jf_token }}"
        status_code: [200, 204]
      when: jf_keys.json.TotalRecordCount == 0

    - name: "Jellyfin | Fetch API key"
      ansible.builtin.uri:
        url: "{{ jellyfin_url }}/Auth/Keys"
        headers:
          X-MediaBrowser-Token: "{{ jf_token }}"
        return_content: true
      register: jf_keys_final

    - name: "Jellyfin | Save credential sync state"
      ansible.builtin.copy:
        dest: "{{ credential_state }}"
        content: "{{ {'api_key': jf_keys_final.json.Items[0].AccessToken, 'users': {}} | to_json }}"
        force: false
        mode: '0600'
      when: jf_keys_final.json.Items | length > 0

    - name: "Jellyfin | Set CORS config"
      ansible.builtin.uri:
        url: "{{ jellyfin_url }}/System/Configuration"
        headers:
          X-MediaBrowser-Token: "{{ jf_token }}"
        return_content: true
      register: jf_config

    - name: "Jellyfin | Update CORS hosts"
      ansible.builtin.uri:
        url: "{{ jellyfin_url }}/System/Configuration"
        method: POST
        headers:
          X-MediaBrowser-Token: "{{ jf_token }}"
        body_format: json
        body: "{{ jf_config.json | combine({'CorsHosts': ['*']}) }}"
        status_code: [200, 204]
      when: "'*' not in (jf_config.json.CorsHosts | default([]))"

    # ── Phase 2: QEMU Guest Agent ─────────────────────────────────────
    - name: "Phase 2 | Start QEMU guest agent"
      ansible.builtin.shell: timeout 5 systemctl start qemu-guest-agent 2>/dev/null; systemctl enable qemu-guest-agent 2>/dev/null; true
      changed_when: false

    # ── Phase 3: Unattended Updates ───────────────────────────────────
    - name: "Phase 3 | Install unattended-upgrades"
      ansible.builtin.apt:
        name:
          - unattended-upgrades
          - apt-listchanges
        state: present

    - name: "Phase 3 | Configure auto-updates periodic"
      ansible.builtin.copy:
        dest: /etc/apt/apt.conf.d/20auto-upgrades
        content: |
          APT::Periodic::Update-Package-Lists "1";
          APT::Periodic::Unattended-Upgrade "1";
          APT::Periodic::AutocleanInterval "7";
        mode: '0644'

    - name: "Phase 3 | Configure unattended-upgrades"
      ansible.builtin.copy:
        dest: /etc/apt/apt.conf.d/50unattended-upgrades
        content: |
          Unattended-Upgrade::Allowed-Origins {
              "${distro_id}:${distro_codename}";
              "${distro_id}:${distro_codename}-security";
              "${distro_id}ESMApps:${distro_codename}-apps-security";
              "${distro_id}ESM:${distro_codename}-infra-security";
          };
          Unattended-Upgrade::AutoFixInterruptedDpkg "true";
          Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
          Unattended-Upgrade::Remove-Unused-Dependencies "true";
          Unattended-Upgrade::Automatic-Reboot "false";
        mode: '0644'

    - name: "Phase 3 | Ensure unattended-upgrades running"
      ansible.builtin.systemd:
        name: unattended-upgrades
        state: started
        enabled: true

    # ── Phase 4: UFW Firewall ─────────────────────────────────────────
    - name: "Phase 4 | Install UFW"
      ansible.builtin.apt:
        name: ufw
        state: present

    - name: "Phase 4 | Set UFW default deny incoming"
      community.general.ufw:
        default: deny
        direction: incoming

    - name: "Phase 4 | Set UFW default allow outgoing"
      community.general.ufw:
        default: allow
        direction: outgoing

    - name: "Phase 4 | Allow SSH"
      community.general.ufw:
        rule: allow
        port: '22'
        proto: tcp
        comment: SSH

    - name: "Phase 4 | Allow HTTP"
      community.general.ufw:
        rule: allow
        port: '80'
        proto: tcp
        comment: "HTTP (Jellyfin + JellyShip)"

    - name: "Phase 4 | Allow SMB"
      community.general.ufw:
        rule: allow
        port: '445'
        proto: tcp
        comment: SMB

    - name: "Phase 4 | Allow NetBIOS Name"
      community.general.ufw:
        rule: allow
        port: '137'
        proto: udp
        comment: "NetBIOS Name"

    - name: "Phase 4 | Allow NetBIOS Datagram"
      community.general.ufw:
        rule: allow
        port: '138'
        proto: udp
        comment: "NetBIOS Datagram"

    - name: "Phase 4 | Allow NetBIOS Session"
      community.general.ufw:
        rule: allow
        port: '139'
        proto: tcp
        comment: "NetBIOS Session"

    - name: "Phase 4 | Enable UFW"
      community.general.ufw:
        state: enabled

    # ── Phase 5: Samba ────────────────────────────────────────────────
    - name: "Phase 5 | Install Samba"
      ansible.builtin.apt:
        name:
          - samba
          - samba-common-bin
        state: present

    - name: "Phase 5 | Configure Samba"
      ansible.builtin.copy:
        dest: /etc/samba/smb.conf
        content: |
          [global]
             workgroup = WORKGROUP
             server string = JellyShip Media Server
             server role = standalone server
             security = user
             map to guest = never
             dns proxy = no
             log file = /var/log/samba/log.%m
             max log size = 1000
             logging = file
             local master = yes
             preferred master = yes
             wins support = yes
             socket options = TCP_NODELAY IPTOS_LOWDELAY SO_RCVBUF=524288 SO_SNDBUF=524288
             read raw = yes
             write raw = yes
             use sendfile = yes
             aio read size = 16384
             aio write size = 16384

          [Media]
             comment = JellyShip Media Library
             path = /srv/jellyship
             browseable = yes
             read only = no
             create mask = 0664
             directory mask = 0775
             valid users = @jellyship-users
             force group = jellyship-users

          [Downloads]
             comment = JellyShip Downloads
             path = /srv/jellyship/downloads
             browseable = yes
             read only = no
             create mask = 0664
             directory mask = 0775
             valid users = @jellyship-users
             force group = jellyship-users
        mode: '0644'
        backup: true
      notify: restart samba

    - name: "Phase 5 | Add jellyship to Samba"
      ansible.builtin.shell: echo -e "{{ jellyship_pass }}\n{{ jellyship_pass }}" | smbpasswd -a -s {{ jellyship_user }}
      args:
        creates: /var/lib/samba/private/passdb.tdb
      changed_when: false

    - name: "Phase 5 | Ensure Samba running"
      ansible.builtin.systemd:
        name: "{{ item }}"
        state: started
        enabled: true
      loop:
        - smbd
        - nmbd

    # ── Phase 5: Credential Sync Service ──────────────────────────────
    - name: "Phase 5 | Ensure credential sync script exists"
      ansible.builtin.file:
        path: /opt/jellyship/jellyship-credential-sync.py
        state: file
        mode: '0755'
      failed_when: false

    - name: "Phase 5 | Install credential sync service"
      ansible.builtin.copy:
        dest: /etc/systemd/system/jellyship-credentials.service
        content: |
          [Unit]
          Description=JellyShip Credential Sync (Jellyfin → Linux + Samba)
          After=network-online.target jellyfin.service
          Wants=network-online.target

          [Service]
          Type=simple
          ExecStart=/usr/bin/python3 /opt/jellyship/jellyship-credential-sync.py
          Restart=on-failure
          RestartSec=10

          [Install]
          WantedBy=multi-user.target
        mode: '0644'
      notify: reload systemd

    - name: "Phase 5 | Ensure credential sync running"
      ansible.builtin.systemd:
        name: jellyship-credentials
        state: started
        enabled: true

    # ── Phase 5: PAM password sync hook ───────────────────────────────
    - name: "Phase 5 | Install PAM password hook"
      ansible.builtin.copy:
        dest: /opt/jellyship/jellyship-passwd-hook.sh
        content: |
          #!/bin/bash
          USER="$PAM_USER"
          if [ -z "$USER" ]; then exit 0; fi
          read -r NEWPASS
          if [ -n "$NEWPASS" ]; then
              echo -e "$NEWPASS\n$NEWPASS" | smbpasswd -s "$USER" 2>/dev/null
          fi
        mode: '0755'

    # NOTE: TTY service is NOT managed here — ansible runs INSIDE the TTY service.
    # Managing it would create a circular dependency (ansible restarts its own parent).
    # The TTY service is installed by preseed and enabled at install time.

  handlers:
    - name: reload systemd
      ansible.builtin.systemd:
        daemon_reload: true

    - name: reload nginx
      ansible.builtin.systemd:
        name: nginx
        state: reloaded

    - name: restart samba
      ansible.builtin.systemd:
        name: "{{ item }}"
        state: restarted
      loop:
        - smbd
        - nmbd

    - name: restart credential sync
      ansible.builtin.systemd:
        name: jellyship-credentials
        state: restarted
