# frozen_string_literal: true

require "faraday"
require "json"
require "uri"

module SynorGovernance
  # Synor Governance SDK client for Ruby.
  # Proposals, voting, DAOs, and vesting operations.
  class Client
    FINAL_STATUSES = [
      ProposalStatus::PASSED,
      ProposalStatus::REJECTED,
      ProposalStatus::EXECUTED,
      ProposalStatus::CANCELLED
    ].freeze

    attr_reader :closed

    def initialize(config)
      @config = config
      @closed = false
      @conn = Faraday.new(url: config.endpoint) do |f|
        f.request :json
        f.response :json
        f.options.timeout = config.timeout
        f.headers["Authorization"] = "Bearer #{config.api_key}"
        f.headers["Content-Type"] = "application/json"
        f.headers["X-SDK-Version"] = "ruby/#{VERSION}"
      end
    end

    # ==================== Proposal Operations ====================

    def create_proposal(draft)
      body = proposal_draft_to_hash(draft)
      response = post("/proposals", body)
      parse_proposal(response)
    end

    def get_proposal(proposal_id)
      response = get("/proposals/#{encode(proposal_id)}")
      parse_proposal(response)
    end

    def list_proposals(filter: nil)
      params = {}
      if filter
        params[:status] = filter.status if filter.status
        params[:proposer] = filter.proposer if filter.proposer
        params[:dao_id] = filter.dao_id if filter.dao_id
        params[:limit] = filter.limit if filter.limit
        params[:offset] = filter.offset if filter.offset
      end
      response = get("/proposals", params)
      (response["proposals"] || []).map { |p| parse_proposal(p) }
    end

    def cancel_proposal(proposal_id)
      response = post("/proposals/#{encode(proposal_id)}/cancel", {})
      parse_proposal(response)
    end

    def execute_proposal(proposal_id)
      response = post("/proposals/#{encode(proposal_id)}/execute", {})
      parse_proposal(response)
    end

    def wait_for_proposal(proposal_id, poll_interval: 60, max_wait: 604_800)
      deadline = Time.now + max_wait
      while Time.now < deadline
        proposal = get_proposal(proposal_id)
        return proposal if FINAL_STATUSES.include?(proposal.status)

        sleep(poll_interval)
      end
      raise Error, "Timeout waiting for proposal completion"
    end

    # ==================== Voting Operations ====================

    def vote(proposal_id:, vote:, weight: nil)
      body = { choice: vote.choice }
      body[:reason] = vote.reason if vote.reason
      body[:weight] = weight if weight
      response = post("/proposals/#{encode(proposal_id)}/vote", body)
      parse_vote_receipt(response)
    end

    def get_votes(proposal_id)
      response = get("/proposals/#{encode(proposal_id)}/votes")
      (response["votes"] || []).map { |v| parse_vote_receipt(v) }
    end

    def get_my_vote(proposal_id)
      response = get("/proposals/#{encode(proposal_id)}/votes/me")
      parse_vote_receipt(response)
    end

    def delegate(delegatee:, amount: nil)
      body = { delegatee: delegatee }
      body[:amount] = amount if amount
      response = post("/voting/delegate", body)
      parse_delegation_receipt(response)
    end

    def undelegate(delegatee)
      response = post("/voting/undelegate", { delegatee: delegatee })
      parse_delegation_receipt(response)
    end

    def get_voting_power(address)
      response = get("/voting/power/#{encode(address)}")
      parse_voting_power(response)
    end

    def get_delegations(address)
      response = get("/voting/delegations/#{encode(address)}")
      (response["delegations"] || []).map { |d| parse_delegation_receipt(d) }
    end

    # ==================== DAO Operations ====================

    def create_dao(config)
      body = dao_config_to_hash(config)
      response = post("/daos", body)
      parse_dao(response)
    end

    def get_dao(dao_id)
      response = get("/daos/#{encode(dao_id)}")
      parse_dao(response)
    end

    def list_daos(limit: nil, offset: nil)
      params = {}
      params[:limit] = limit if limit
      params[:offset] = offset if offset
      response = get("/daos", params)
      (response["daos"] || []).map { |d| parse_dao(d) }
    end

    def get_dao_treasury(dao_id)
      response = get("/daos/#{encode(dao_id)}/treasury")
      parse_dao_treasury(response)
    end

    def get_dao_members(dao_id)
      response = get("/daos/#{encode(dao_id)}/members")
      response["members"] || []
    end

    # ==================== Vesting Operations ====================

    def create_vesting_schedule(schedule)
      body = vesting_schedule_to_hash(schedule)
      response = post("/vesting", body)
      parse_vesting_contract(response)
    end

    def get_vesting_contract(contract_id)
      response = get("/vesting/#{encode(contract_id)}")
      parse_vesting_contract(response)
    end

    def list_vesting_contracts(beneficiary: nil)
      path = beneficiary ? "/vesting?beneficiary=#{encode(beneficiary)}" : "/vesting"
      response = get(path)
      (response["contracts"] || []).map { |c| parse_vesting_contract(c) }
    end

    def claim_vested(contract_id)
      response = post("/vesting/#{encode(contract_id)}/claim", {})
      parse_claim_receipt(response)
    end

    def revoke_vesting(contract_id)
      response = post("/vesting/#{encode(contract_id)}/revoke", {})
      parse_vesting_contract(response)
    end

    def get_releasable_amount(contract_id)
      response = get("/vesting/#{encode(contract_id)}/releasable")
      response["amount"]
    end

    # ==================== Lifecycle ====================

    def health_check
      response = get("/health")
      response["status"] == "healthy"
    rescue StandardError
      false
    end

    def close
      @closed = true
      @conn.close if @conn.respond_to?(:close)
    end

    private

    def get(path, params = {})
      execute { @conn.get(path, params).body }
    end

    def post(path, body)
      execute { @conn.post(path, body).body }
    end

    def execute
      raise ClientClosedError, "Client has been closed" if @closed

      last_error = nil
      @config.retries.times do |attempt|
        begin
          response = yield
          check_error(response) if response.is_a?(Hash)
          return response
        rescue StandardError => e
          last_error = e
          sleep(2**attempt) if attempt < @config.retries - 1
        end
      end
      raise last_error
    end

    def check_error(response)
      return unless response["error"] || (response["code"] && response["message"])

      message = response["message"] || response["error"] || "Unknown error"
      code = response["code"]
      status = response["status_code"] || 0
      raise HttpError.new(message, status_code: status, code: code)
    end

    def encode(str)
      URI.encode_www_form_component(str)
    end

    # Parsing methods

    def parse_proposal(data)
      Proposal.new(
        id: data["id"],
        title: data["title"],
        description: data["description"],
        discussion_url: data["discussion_url"],
        proposer: data["proposer"],
        status: data["status"],
        created_at: data["created_at"],
        voting_start_time: data["voting_start_time"],
        voting_end_time: data["voting_end_time"],
        execution_time: data["execution_time"],
        vote_tally: data["vote_tally"] ? parse_vote_tally(data["vote_tally"]) : nil,
        actions: (data["actions"] || []).map { |a| parse_proposal_action(a) },
        dao_id: data["dao_id"]
      )
    end

    def parse_vote_tally(data)
      VoteTally.new(
        for_votes: data["for_votes"],
        against_votes: data["against_votes"],
        abstain_votes: data["abstain_votes"],
        quorum: data["quorum"],
        quorum_required: data["quorum_required"],
        total_voters: data["total_voters"]
      )
    end

    def parse_proposal_action(data)
      ProposalAction.new(
        target: data["target"],
        method: data["method"],
        data: data["data"],
        value: data["value"]
      )
    end

    def parse_vote_receipt(data)
      VoteReceipt.new(
        id: data["id"],
        proposal_id: data["proposal_id"],
        voter: data["voter"],
        choice: data["choice"],
        weight: data["weight"],
        reason: data["reason"],
        voted_at: data["voted_at"],
        tx_hash: data["tx_hash"]
      )
    end

    def parse_voting_power(data)
      VotingPower.new(
        address: data["address"],
        delegated_power: data["delegated_power"],
        own_power: data["own_power"],
        total_power: data["total_power"],
        delegators: data["delegators"] || []
      )
    end

    def parse_delegation_receipt(data)
      DelegationReceipt.new(
        id: data["id"],
        from: data["from"],
        to: data["to"],
        amount: data["amount"],
        delegated_at: data["delegated_at"],
        tx_hash: data["tx_hash"]
      )
    end

    def parse_dao(data)
      Dao.new(
        id: data["id"],
        name: data["name"],
        description: data["description"],
        type: data["type"],
        token_address: data["token_address"],
        voting_period_days: data["voting_period_days"],
        timelock_days: data["timelock_days"],
        quorum_percent: data["quorum_percent"],
        proposal_threshold: data["proposal_threshold"],
        total_proposals: data["total_proposals"],
        active_proposals: data["active_proposals"],
        treasury_value: data["treasury_value"],
        member_count: data["member_count"],
        created_at: data["created_at"]
      )
    end

    def parse_dao_treasury(data)
      DaoTreasury.new(
        dao_id: data["dao_id"],
        total_value: data["total_value"],
        tokens: (data["tokens"] || []).map { |t| parse_treasury_token(t) },
        last_updated: data["last_updated"]
      )
    end

    def parse_treasury_token(data)
      TreasuryToken.new(
        address: data["address"],
        balance: data["balance"],
        name: data["name"],
        symbol: data["symbol"]
      )
    end

    def parse_vesting_contract(data)
      VestingContract.new(
        id: data["id"],
        beneficiary: data["beneficiary"],
        grantor: data["grantor"],
        total_amount: data["total_amount"],
        released_amount: data["released_amount"],
        releasable_amount: data["releasable_amount"],
        start_time: data["start_time"],
        cliff_time: data["cliff_time"],
        end_time: data["end_time"],
        status: data["status"],
        revocable: data["revocable"],
        created_at: data["created_at"]
      )
    end

    def parse_claim_receipt(data)
      ClaimReceipt.new(
        id: data["id"],
        contract_id: data["contract_id"],
        amount: data["amount"],
        tx_hash: data["tx_hash"],
        claimed_at: data["claimed_at"]
      )
    end

    # Conversion methods

    def proposal_draft_to_hash(draft)
      hash = { title: draft.title, description: draft.description }
      hash[:discussion_url] = draft.discussion_url if draft.discussion_url
      hash[:voting_start_time] = draft.voting_start_time if draft.voting_start_time
      hash[:voting_end_time] = draft.voting_end_time if draft.voting_end_time
      hash[:dao_id] = draft.dao_id if draft.dao_id
      hash[:actions] = draft.actions.map { |a| proposal_action_to_hash(a) } if draft.actions
      hash
    end

    def proposal_action_to_hash(action)
      { target: action.target, method: action.method, data: action.data, value: action.value }
    end

    def dao_config_to_hash(config)
      hash = {
        name: config.name,
        description: config.description,
        type: config.type,
        voting_period_days: config.voting_period_days,
        timelock_days: config.timelock_days
      }
      hash[:token_address] = config.token_address if config.token_address
      hash[:quorum_percent] = config.quorum_percent if config.quorum_percent
      hash[:proposal_threshold] = config.proposal_threshold if config.proposal_threshold
      hash[:multisig_members] = config.multisig_members if config.multisig_members
      hash[:multisig_threshold] = config.multisig_threshold if config.multisig_threshold
      hash
    end

    def vesting_schedule_to_hash(schedule)
      {
        beneficiary: schedule.beneficiary,
        total_amount: schedule.total_amount,
        start_time: schedule.start_time,
        cliff_duration: schedule.cliff_duration,
        vesting_duration: schedule.vesting_duration,
        revocable: schedule.revocable
      }
    end
  end
end
