Clone
defmodule Mix.Tasks.Deploy do
  use Mix.Task

  @shortdoc "Deploy Phoenix application to VPS"
  @moduledoc """
  Deploy Phoenix application to a VPS using SSH.

  ## Usage

      mix deploy                      # Deploy to production (default)
      mix deploy staging              # Deploy to staging
      mix deploy staging feature-xyz  # Deploy specific branch to staging

  ## Configuration

  Configure deployment in `config/deploy.exs`:

      import Config

      config :deploy,
        repository: "git@github.com:username/app-name.git",
        shared_dirs: ["uploads", "tmp", "logs"],
        shared_files: [".env"],
        build_script: "./build.sh"

      config :deploy, :production,
        branch: "main",
        user: "dev",
        domain: "ssh.example.com",
        port: 22,
        deploy_to: "/var/www/app-name",
        url: "https://app.example.com",
        app_port: 4000

      config :deploy, :staging,
        branch: "develop",
        user: "dev",
        domain: "ssh.example.com",
        port: 22,
        deploy_to: "/var/www/staging.app-name",
        url: "https://staging.app.example.com",
        app_port: 4001
  """

  require Logger

  @impl Mix.Task
  def run(args) do
    # Load deployment config
    Mix.Task.run("loadconfig", ["config/deploy.exs"])
    
    {env, branch} = parse_args(args)
    config = get_deploy_config(env)
    
    # Allow branch override
    config = if branch, do: Map.put(config, :branch, branch), else: config
    
    Logger.info("Deploying to #{env} environment...")
    Logger.info("Branch: #{config.branch}")
    Logger.info("Host: #{config.user}@#{config.domain}:#{config.port}")
    Logger.info("Deploy to: #{config.deploy_to}")
    
    # Execute deployment
    with :ok <- check_requirements(config),
         {:ok, config} <- create_release_dir(config),
         :ok <- clone_or_fetch_code(config),
         :ok <- link_shared_paths(config),
         :ok <- run_build_script(config),
         :ok <- update_symlinks(config),
         :ok <- restart_service(config),
         :ok <- cleanup_old_releases(config),
         :ok <- health_check(config) do
      Logger.info("✅ Deployment completed successfully!")
    else
      {:error, reason} ->
        Logger.error("❌ Deployment failed: #{reason}")
        exit(1)
    end
  end

  defp parse_args([]), do: {:production, nil}
  defp parse_args([env]), do: {String.to_atom(env), nil}
  defp parse_args([env, branch]), do: {String.to_atom(env), branch}

  defp get_deploy_config(env), do: Deploy.SSH.get_deploy_config(env)

  defp check_requirements(config) do
    required_keys = [:repository, :user, :domain, :deploy_to, :branch]
    missing = Enum.filter(required_keys, &(not Map.has_key?(config, &1)))
    
    if Enum.empty?(missing) do
      :ok
    else
      {:error, "Missing required config keys: #{inspect(missing)}"}
    end
  end

  defp create_release_dir(config) do
    timestamp = DateTime.utc_now() |> DateTime.to_unix() |> to_string()
    release_path = "#{config.deploy_to}/releases/#{timestamp}"
    
    case ssh_exec(config, "mkdir -p #{release_path}") do
      {_, 0} -> 
        config = Map.put(config, :release_path, release_path)
        {:ok, config}
      {output, _} -> 
        {:error, "Failed to create release directory: #{output}"}
    end
  end

  defp clone_or_fetch_code(%{release_path: release_path} = config) do
    repo_path = "#{config.deploy_to}/repo"
    
    # Clone or update repository
    clone_cmd = """
    if [ -d #{repo_path} ]; then
      cd #{repo_path} && git fetch origin
    else
      git clone #{config.repository} #{repo_path}
    fi
    """
    
    case ssh_exec(config, clone_cmd) do
      {_, 0} ->
        # Archive the specific branch to release directory (use origin/ to get fetched code)
        archive_cmd = """
        cd #{repo_path} && \
        git archive origin/#{config.branch} | tar -x -C #{release_path}
        """
        
        case ssh_exec(config, archive_cmd) do
          {_, 0} -> :ok
          {output, _} -> {:error, "Failed to archive code: #{output}"}
        end
      {output, _} ->
        {:error, "Failed to clone/fetch repository: #{output}"}
    end
  end

  defp link_shared_paths(%{release_path: release_path} = config) do
    shared_path = "#{config.deploy_to}/shared"
    
    # Create shared directories if they don't exist
    shared_dirs = Map.get(config, :shared_dirs, [])
    shared_files = Map.get(config, :shared_files, [])
    
    # Create shared structure
    create_shared_cmd = """
    mkdir -p #{shared_path} && \
    #{Enum.map(shared_dirs, fn dir -> "mkdir -p #{shared_path}/#{dir}" end) |> Enum.join(" && ")}
    """
    
    case ssh_exec(config, create_shared_cmd) do
      {_, 0} ->
        # Link shared directories
        link_dirs_cmd = shared_dirs
        |> Enum.map(fn dir -> 
          "ln -nfs #{shared_path}/#{dir} #{release_path}/#{dir}"
        end)
        |> Enum.join(" && ")
        
        # Link shared files
        link_files_cmd = shared_files
        |> Enum.map(fn file ->
          dir = Path.dirname(file)
          "mkdir -p #{release_path}/#{dir} && " <>
          "if [ -f #{shared_path}/#{file} ]; then " <>
          "ln -nfs #{shared_path}/#{file} #{release_path}/#{file}; " <>
          "fi"
        end)
        |> Enum.join(" && ")
        
        commands = [link_dirs_cmd, link_files_cmd]
        |> Enum.reject(&(&1 == ""))
        
        if Enum.empty?(commands) do
          :ok
        else
          full_cmd = Enum.join(commands, " && ")
          case ssh_exec(config, full_cmd) do
            {_, 0} -> :ok
            {output, _} -> {:error, "Failed to link shared paths: #{output}"}
          end
        end
      {output, _} ->
        {:error, "Failed to create shared directories: #{output}"}
    end
  end

  defp run_build_script(%{release_path: release_path} = config) do
    build_script = Map.get(config, :build_script, "./build.sh")
    
    Logger.info("Running build script...")
    
    # First make it executable
    chmod_cmd = "chmod +x #{release_path}/#{build_script}"
    case ssh_exec(config, chmod_cmd) do
      {_, 0} ->
        # Run the build script with explicit bash to ensure profile is loaded
        build_cmd = "cd #{release_path} && /bin/bash -l -c '#{build_script}'"
        
        case ssh_exec(config, build_cmd) do
          {_, 0} -> :ok
          {output, _} -> {:error, "Build failed: #{output}"}
        end
      {output, _} ->
        {:error, "Failed to make build script executable: #{output}"}
    end
  end

  defp update_symlinks(%{release_path: release_path} = config) do
    current_path = "#{config.deploy_to}/current"
    
    # Update current symlink atomically
    update_cmd = """
    ln -nfs #{release_path} #{current_path}.tmp && \
    mv -Tf #{current_path}.tmp #{current_path}
    """
    
    case ssh_exec(config, update_cmd) do
      {_, 0} -> :ok
      {output, _} -> {:error, "Failed to update symlinks: #{output}"}
    end
  end

  defp restart_service(config) do
    # Service name is {app_name}-{env} (e.g., testapp-staging, testapp-production)
    service_name = "#{config.app_name}-#{config.env}"

    restart_cmd = """
    sudo systemctl daemon-reload && \
    sudo systemctl restart #{service_name} && \
    sudo systemctl status #{service_name} | head -n 10
    """

    Logger.info("Restarting service: #{service_name}")

    case ssh_exec(config, restart_cmd) do
      {output, 0} ->
        Logger.info(output)
        :ok
      {output, _} ->
        {:error, "Failed to restart service: #{output}"}
    end
  end

  defp cleanup_old_releases(config) do
    # Keep only the last 5 releases
    cleanup_cmd = """
    cd #{config.deploy_to}/releases && \
    ls -t | tail -n +6 | xargs -r rm -rf
    """
    
    case ssh_exec(config, cleanup_cmd) do
      {_, 0} -> :ok
      {_, _} -> 
        Logger.warning("Failed to cleanup old releases")
        :ok  # Don't fail deployment for cleanup
    end
  end

  defp health_check(config) do
    if url = Map.get(config, :url) do
      Logger.info("Performing health check...")
      
      health_cmd = "curl -fsSL -o /dev/null -w '%{http_code}' --retry 3 --retry-delay 2 #{url}"
      
      case ssh_exec(config, health_cmd) do
        {"200", 0} -> 
          Logger.info("✅ Health check passed!")
          :ok
        {status, 0} ->
          Logger.warning("Health check returned status: #{status}")
          :ok  # Don't fail for non-200 status
        {output, _} ->
          Logger.warning("Health check failed: #{output}")
          :ok  # Don't fail deployment for health check
      end
    else
      :ok
    end
  end

  defp ssh_exec(config, command, _opts \\ []), do: Deploy.SSH.ssh_exec(config, command)

  defp scp_upload(config, local_file, remote_file), do: Deploy.SSH.scp_upload(config, local_file, remote_file)
end