module Scoring
  class ApplyAction
    Result = Struct.new(:score, :set_won, :match_won, :winner, keyword_init: true)

    def initialize(session:, action:)
      @session = session
      @match = session.match
      @action = action.to_s
      @rules = Rules.from_match(@match)
    end

    def call
      score = @session.score_state || @session.create_score_state!(
        home_sets: 0, away_sets: 0, home_points: 0, away_points: 0, current_set: 1, set_partials: []
      )

      case @action
      when "home_point"
        apply_point(score, :home)
      when "away_point"
        apply_point(score, :away)
      when "home_undo"
        apply_undo(score, :home)
      when "away_undo"
        apply_undo(score, :away)
      when "close_set"
        apply_close_set(score)
      else
        raise ArgumentError, "Azione non valida"
      end
    end

    private

    def apply_point(score, side)
      if side == :home
        score.home_points += 1
      else
        score.away_points += 1
      end
      score.save!
      broadcast(score)
      winner = @rules.set_winner_from_points(
        home_points: score.home_points,
        away_points: score.away_points,
        current_set: score.current_set
      )
      Result.new(score: score, set_won: winner.present?, match_won: false, winner: winner)
    end

    def apply_undo(score, side)
      if side == :home
        score.home_points = [score.home_points - 1, 0].max
      else
        score.away_points = [score.away_points - 1, 0].max
      end
      score.save!
      broadcast(score)
      Result.new(score: score, set_won: false, match_won: false, winner: nil)
    end

    def apply_close_set(score)
      winner = @rules.set_winner_from_points(
        home_points: score.home_points,
        away_points: score.away_points,
        current_set: score.current_set
      )
      winner ||= score.home_points > score.away_points ? :home : :away if score.home_points != score.away_points
      return Result.new(score: score, set_won: false, match_won: false, winner: nil) unless winner

      close_set_for(score, winner)
    end

    def close_set_for(score, winner)
      partials = Array(score.set_partials)
      partials << {
        "set" => score.current_set,
        "home" => score.home_points,
        "away" => score.away_points
      }

      if winner == :home
        score.home_sets += 1
      else
        score.away_sets += 1
      end

      match_winner = @rules.match_winner(home_sets: score.home_sets, away_sets: score.away_sets)

      score.assign_attributes(
        set_partials: partials,
        home_points: 0,
        away_points: 0,
        current_set: score.current_set + 1
      )
      score.save!
      broadcast(score)
      Result.new(
        score: score,
        set_won: true,
        match_won: match_winner.present?,
        winner: match_winner || winner
      )
    end

    def broadcast(score)
      SessionChannel.broadcast_message(@session, score.as_cable_payload)
    end
  end
end
