lib/kitsune/kit/commands/setup_postgres_docker.rb



require "thor"
require "net/ssh"
require "tempfile"
require "fileutils"
require "shellwords"
require_relative "../defaults"
require_relative "../options_builder"

module Kitsune
  module Kit
    module Commands
      class SetupPostgresDocker < Thor
        namespace "setup_postgres_docker"

        class_option :server_ip, aliases: "-s", required: true, desc: "Server IP address or hostname"
        class_option :ssh_port, aliases: "-p", desc: "SSH port"
        class_option :ssh_key_path, aliases: "-k", desc: "Path to SSH private key"

        desc "create", "Setup PostgreSQL using Docker Compose on remote server"
        def create
          postgres_defaults = Kitsune::Kit::Defaults.postgres

          if postgres_defaults[:postgres_password] == "secret"
            say "โš ๏ธ Warning: You are using the default PostgreSQL password ('secret').", :yellow
            if ENV.fetch("KIT_ENV", "development") == "production"
              abort "โŒ Production environment requires a secure PostgreSQL password!"
            else
              say "๐Ÿ”’ Please change POSTGRES_PASSWORD in your .env if needed.", :yellow
            end
          end

          filled_options = Kitsune::Kit::OptionsBuilder.build(
            options,
            required: [:server_ip],
            defaults: Kitsune::Kit::Defaults.ssh
          )

          with_ssh_connection(filled_options) do |ssh|
            perform_setup(ssh, postgres_defaults)

            database_url = build_database_url(filled_options, postgres_defaults)
            say "๐Ÿ”— Your DATABASE_URL is:\t", :cyan
            say database_url, :green
          end
        end

        desc "rollback", "Remove PostgreSQL Docker setup from remote server"
        def rollback
          postgres_defaults = Kitsune::Kit::Defaults.postgres

          filled_options = Kitsune::Kit::OptionsBuilder.build(
            options,
            required: [:server_ip],
            defaults: Kitsune::Kit::Defaults.ssh
          )

          with_ssh_connection(filled_options) do |ssh|
            perform_rollback(ssh, postgres_defaults)
          end
        end

        no_commands do
          def with_ssh_connection(filled_options)
            server = filled_options[:server_ip]
            port   = filled_options[:ssh_port]
            key    = File.expand_path(filled_options[:ssh_key_path])

            say "๐Ÿ”‘ Connecting as deploy@#{server}:#{port}", :green
            Net::SSH.start(server, "deploy", port: port, keys: [key], non_interactive: true, timeout: 5) do |ssh|
              yield ssh
            end
          end

          def perform_setup(ssh, postgres_defaults)
            docker_compose_local = ".kitsune/docker/postgres.yml"
            unless File.exist?(docker_compose_local)
              say "โŒ Docker compose file not found at #{docker_compose_local}.", :red
              exit(1)
            end

            docker_dir_remote = "$HOME/docker/postgres"
            docker_compose_remote = "#{docker_dir_remote}/docker-compose.yml"
            docker_env_remote = "#{docker_dir_remote}/.env"
            backup_marker = "/usr/local/backups/setup_postgres_docker.after"

            # 1. Create base directory securely
            ssh.exec!("mkdir -p #{docker_dir_remote}")
            ssh.exec!("chmod 700 #{docker_dir_remote}")

            # 2. Upload docker-compose.yml
            say "๐Ÿ“ฆ Uploading docker-compose.yml to remote server...", :cyan
            content_compose = File.read(docker_compose_local)
            upload_file(ssh, content_compose, docker_compose_remote)

            # 3. Create .env file for docker-compose based on postgres_defaults
            say "๐Ÿ“ฆ Creating .env file for Docker Compose...", :cyan
            env_content = <<~ENVFILE
              POSTGRES_DB=#{postgres_defaults[:postgres_db]}
              POSTGRES_USER=#{postgres_defaults[:postgres_user]}
              POSTGRES_PASSWORD=#{postgres_defaults[:postgres_password]}
              POSTGRES_PORT=#{postgres_defaults[:postgres_port]}
              POSTGRES_IMAGE=#{postgres_defaults[:postgres_image]}
            ENVFILE
            upload_file(ssh, env_content, docker_env_remote)

            # 4. Secure file permissions
            ssh.exec!("chmod 600 #{docker_compose_remote} #{docker_env_remote}")

            # 5. Create backup marker
            ssh.exec!("sudo mkdir -p /usr/local/backups && sudo touch #{backup_marker}")

            # 6. Validate docker-compose.yml
            say "๐Ÿ” Validating docker-compose.yml...", :cyan
            validation_output = ssh.exec!("cd #{docker_dir_remote} && docker compose config")
            say validation_output, :cyan

            # 7. Check if container is running
            container_status = ssh.exec!("docker ps --filter 'name=postgres' --format '{{.Status}}'").strip

            if container_status.empty?
              say "โ–ถ๏ธ No running container. Running docker compose up...", :cyan
              ssh.exec!("cd #{docker_dir_remote} && docker compose up -d")
            else
              say "โš ๏ธ PostgreSQL container is already running.", :yellow
              if yes?("๐Ÿ” Recreate the container with updated configuration? [y/N]", :yellow)
                say "๐Ÿ”„ Recreating container...", :cyan
                ssh.exec!("cd #{docker_dir_remote} && docker compose down -v && docker compose up -d")
              else
                say "โฉ Keeping existing container.", :cyan
              end
            end

            say "๐Ÿ“‹ Final container status (docker compose ps):", :cyan
            docker_ps_output = ssh.exec!("cd #{docker_dir_remote} && docker compose ps --format json")

            if docker_ps_output.nil? || docker_ps_output.strip.empty? || docker_ps_output.include?("no configuration file")
              say "โš ๏ธ docker compose ps returned no valid output.", :yellow
            else
              begin
                services = JSON.parse(docker_ps_output)
                services = [services] if services.is_a?(Hash)

                postgres = services.find { |svc| svc["Service"] == "postgres" }
                status = postgres && postgres["State"]
                health = postgres && postgres["Health"]

                if (status == "running" && health == "healthy") || (health == "healthy")
                  say "โœ… PostgreSQL container is running and healthy.", :green
                else
                  say "โš ๏ธ PostgreSQL container is not healthy yet.", :yellow
                end
              rescue JSON::ParserError => e
                say "๐Ÿšจ Failed to parse docker compose ps output as JSON: #{e.message}", :red
              end
            end

            # 9. Check PostgreSQL readiness with retries
            say "๐Ÿ” Checking PostgreSQL health with retries...", :cyan

            max_attempts = 10
            attempt = 0
            success = false

            while attempt < max_attempts
              attempt += 1
              healthcheck = ssh.exec!("docker exec $(docker ps -qf name=postgres) pg_isready -U #{postgres_defaults[:postgres_user]} -d #{postgres_defaults[:postgres_db]} -h localhost")

              if healthcheck.include?("accepting connections")
                say "โœ… PostgreSQL is up and accepting connections! (attempt #{attempt})", :green
                success = true
                break
              else
                say "โณ PostgreSQL not ready yet, retrying in 5 seconds... (#{attempt + 1}/#{max_attempts})", :yellow
                sleep 5
              end
            end

            unless success
              say "โŒ PostgreSQL did not become ready after #{max_attempts} attempts.", :red
            end

            # 10. Allow PostgreSQL port through firewall (ufw)
            say "๐Ÿ›ก๏ธ Configuring firewall to allow PostgreSQL (port #{postgres_defaults[:postgres_port]})...", :cyan
            firewall = <<~EOH
              if command -v ufw >/dev/null; then
                if ! sudo ufw status | grep -q "#{postgres_defaults[:postgres_port]}"; then
                  sudo ufw allow #{postgres_defaults[:postgres_port]}
                else
                  echo "๐Ÿ’ก Port #{postgres_defaults[:postgres_port]} is already allowed in ufw."
                fi
              else
                echo "โš ๏ธ ufw not found. Skipping firewall configuration."
              fi
            EOH
            ssh.exec!(firewall)
          end

          def perform_rollback(ssh, postgres_defaults)
            output = ssh.exec! <<~EOH
              set -e

              BASE_DIR="$HOME/docker/postgres"
              BACKUP_DIR="/usr/local/backups"
              SCRIPT_ID="setup_postgres_docker"
              AFTER_FILE="${BACKUP_DIR}/${SCRIPT_ID}.after"

              if [ -f "$AFTER_FILE" ]; then
                echo "๐Ÿ” Stopping and removing docker containers..."
                cd "$BASE_DIR"
                docker compose down -v || true

                echo "๐Ÿงน Cleaning up files..."
                rm -rf "$BASE_DIR"
                sudo rm -f "$AFTER_FILE"

                if command -v ufw >/dev/null; then
                  echo "๐Ÿ›ก๏ธ Removing PostgreSQL port from firewall..."
                  sudo ufw delete allow #{postgres_defaults[:postgres_port]} || true
                fi
              else
                echo "๐Ÿ’ก Nothing to rollback"
              fi

              echo "โœ… Rollback completed"
            EOH
            say output
          end

          def upload_file(ssh, content, remote_path)
            escaped_content = Shellwords.escape(content)
            ssh.exec!("mkdir -p #{File.dirname(remote_path)}")
            ssh.exec!("echo #{escaped_content} > #{remote_path}")
          end

          def build_database_url(filled_options, postgres_defaults)
            user = postgres_defaults[:postgres_user]
            password = postgres_defaults[:postgres_password]
            host = filled_options[:server_ip]
            port = postgres_defaults[:postgres_port]
            db = postgres_defaults[:postgres_db]

            "postgres://#{user}:#{password}@#{host}:#{port}/#{db}"
          end
        end
      end
    end
  end
end