(use-trait ft-trait 'SP1Z92MPDQEWZXW36VX71Q25HKF5K2EPCJ304F275.sip-010-v1a.sip-010-trait)

;; stackswap governance
;; 
;; Can see, vote and submit a new proposal
;; A proposal will just update the DAO with new contracts.

;; Errors
(define-constant ERR-NOT-ENOUGH-BALANCE u3401)
(define-constant ERR-NO-CONTRACT-CHANGES u3402)
(define-constant ERR-WRONG-TOKEN u3403)
(define-constant ERR-EMERGENCY-SHUTDOWN-ACTIVATED u3404)
(define-constant ERR-BLOCK-HEIGHT-NOT-REACHED u3405)
(define-constant ERR-NOT-AUTHORIZED u3406)
(define-constant STATUS-OK u0)
(define-constant PROPOSAL-IS-NOT-OPEN u3407)
(define-constant ERR-BLOCK-PASSED u3408)
(define-constant ERR-DAO-GET u3409)


;; Constants
(define-constant DAO-OWNER tx-sender)

;; Proposal variables
(define-map proposals
  { id: uint }
  {
    id: uint,
    proposer: principal,
    title: (string-utf8 256),
    url: (string-utf8 256),
    is-open: bool,
    start-block-height: uint,
    end-block-height: uint,
    yes-votes: uint,
    no-votes: uint,
    contract-changes: (list 10 (tuple (name (string-ascii 256)) (address principal) (qualified-name principal) (can-mint bool) (can-burn bool)))
  }
)

(define-data-var proposal-count uint u0)
(define-data-var proposal-ids (list 100 uint) (list u0))

(define-map votes-by-member { proposal-id: uint, member: principal } { vote-count: uint })
(define-map tokens-by-member { proposal-id: uint, member: principal, token: principal } { amount: uint })

;; Get all proposals
(define-read-only (get-proposals)
  (ok (map get-proposal-by-id (var-get proposal-ids)))
)

;; Get all proposal IDs
(define-read-only (get-proposal-ids)
  (ok (var-get proposal-ids))
)

;; Get votes for a member on proposal
(define-read-only (get-votes-by-member-by-id (proposal-id uint) (member principal))
  (default-to 
    { vote-count: u0 }
    (map-get? votes-by-member { proposal-id: proposal-id, member: member })
  )
)

(define-read-only (get-tokens-by-member-by-id (proposal-id uint) (member principal) (token <ft-trait>))
  (default-to 
    { amount: u0 }
    (map-get? tokens-by-member { proposal-id: proposal-id, member: member, token: (contract-of token) }) 
  )
)

;; Get proposal
(define-read-only (get-proposal-by-id (proposal-id uint))
  (default-to
    {
      id: u0,
      proposer: DAO-OWNER,
      title: u"",
      url: u"",
      is-open: false,
      start-block-height: u0,
      end-block-height: u0,
      yes-votes: u0,
      no-votes: u0,
      contract-changes: (list { name: "", address: DAO-OWNER, qualified-name: DAO-OWNER, can-mint: false, can-burn: false} )
    }
    (map-get? proposals { id: proposal-id })
  )
)

;; To check which tokens are accepted as votes
(define-read-only (is-token-accepted (token <ft-trait>))
  (let (
    (is-stsw (is-eq (contract-of token) 'SP1Z92MPDQEWZXW36VX71Q25HKF5K2EPCJ304F275.stsw-token-v3a))
    (is-vstsw (is-eq (contract-of token) 'SP1Z92MPDQEWZXW36VX71Q25HKF5K2EPCJ304F275.vstsw-token-v1a))
  )
    (or is-stsw  is-vstsw)
  )
)

;; Start a proposal
;; Requires 1% of the supply in your wallet
;; Default voting period is 5 days (144 * 5 blocks) // templory 4 blocks
(define-public (propose
    (start-block-height uint)
    (title (string-utf8 256))
    (url (string-utf8 256))
    (contract-changes (list 10 (tuple (name (string-ascii 256)) (address principal) (qualified-name principal) (can-mint bool) (can-burn bool))))
  )
  (let (
    (proposer-balance (unwrap-panic (contract-call? 'SP1Z92MPDQEWZXW36VX71Q25HKF5K2EPCJ304F275.stsw-token-v3a get-balance tx-sender)))
    (supply (- (unwrap-panic (contract-call? 'SP1Z92MPDQEWZXW36VX71Q25HKF5K2EPCJ304F275.stsw-token-v3a get-total-supply)) u200000000))
    (proposal-id (+ u1 (var-get proposal-count)))
  )
    (asserts! (<= block-height start-block-height) (err ERR-BLOCK-PASSED))
    ;; Requires 1% of the supply 
    (asserts! (>= (* proposer-balance u100) supply) (err ERR-NOT-ENOUGH-BALANCE))
    ;; Mutate

    (asserts! (is-eq (unwrap! (contract-call? 'SP1Z92MPDQEWZXW36VX71Q25HKF5K2EPCJ304F275.stackswap-dao-v4a get-qualified-name-by-name "governance") (err ERR-DAO-GET)) (as-contract tx-sender)) (err ERR-NOT-AUTHORIZED))

    (map-set proposals
      { id: proposal-id }
      {
        id: proposal-id,
        proposer: tx-sender,
        title: title,
        url: url,
        is-open: true,
        start-block-height: start-block-height,
        end-block-height: (+ start-block-height u4),
        yes-votes: u0,
        no-votes: u0,
        contract-changes: contract-changes
      }
    )
    (var-set proposal-count proposal-id)
    (var-set proposal-ids (unwrap-panic (as-max-len? (append (var-get proposal-ids) proposal-id) u100)))
    (ok true)
  )
)

(define-public (vote-for (token <ft-trait>) (proposal-id uint) (amount uint))
  (let (
    (proposal (get-proposal-by-id proposal-id))
    (vote-count (get vote-count (get-votes-by-member-by-id proposal-id tx-sender)))
    (token-count (get amount (get-tokens-by-member-by-id proposal-id tx-sender token)))
  )
    (asserts! (is-eq (is-token-accepted token) true) (err ERR-WRONG-TOKEN))
    ;; Proposal should be open for voting
    (asserts! (is-eq (get is-open proposal) true) (err PROPOSAL-IS-NOT-OPEN))
    ;; Vote should be casted after the start-block-height
    (asserts! (>= block-height (get start-block-height proposal)) (err ERR-NOT-AUTHORIZED))
    ;; Voter should be able to stake
    (try! (contract-call? token transfer amount tx-sender (as-contract tx-sender) none))
    ;; Mutate
    (map-set proposals
      { id: proposal-id }
      (merge proposal { yes-votes: (+ amount (get yes-votes proposal)) }))
    (map-set votes-by-member 
      { proposal-id: proposal-id, member: tx-sender }
      { vote-count: (+ vote-count amount) })
    (map-set tokens-by-member
      { proposal-id: proposal-id, member: tx-sender, token: (contract-of token) }
      { amount: (+ token-count amount) })

    (ok true)
  )
)

(define-public (vote-against (token <ft-trait>) (proposal-id uint) (amount uint))
  (let (
    (proposal (get-proposal-by-id proposal-id))
    (vote-count (get vote-count (get-votes-by-member-by-id proposal-id tx-sender)))
    (token-count (get amount (get-tokens-by-member-by-id proposal-id tx-sender token)))
  )

    (asserts! (is-eq (is-token-accepted token) true) (err ERR-WRONG-TOKEN))
    ;; Proposal should be open for voting
    (asserts! (is-eq (get is-open proposal) true) (err PROPOSAL-IS-NOT-OPEN))
    ;; Vote should be casted after the start-block-height
    (asserts! (>= block-height (get start-block-height proposal)) (err ERR-NOT-AUTHORIZED))
    ;; Voter should be able to stake
    (try! (contract-call? token transfer amount tx-sender (as-contract tx-sender) none ))
    ;; Mutate
    (map-set proposals
      { id: proposal-id }
      (merge proposal { no-votes: (+ amount (get no-votes proposal)) }))
    (map-set votes-by-member 
      { proposal-id: proposal-id, member: tx-sender }
      { vote-count: (+ vote-count amount) })
    (map-set tokens-by-member
      { proposal-id: proposal-id, member: tx-sender, token: (contract-of token) }
      { amount: (+ token-count amount) })
    (ok true)
  )
)

(define-public (end-proposal (proposal-id uint))
  (let ((proposal (get-proposal-by-id proposal-id)))

    (asserts! (not (is-eq (get id proposal) u0)) (err ERR-NOT-AUTHORIZED))
    (asserts! (is-eq (get is-open proposal) true) (err PROPOSAL-IS-NOT-OPEN))
    (asserts! (>= block-height (get end-block-height proposal)) (err ERR-BLOCK-HEIGHT-NOT-REACHED))

    (map-set proposals
      { id: proposal-id }
      (merge proposal { is-open: false }))
    (if (> (get yes-votes proposal) (get no-votes proposal))
      (try! (execute-proposal proposal-id))
      false
    )
    (ok true)
  )
)

;; Return votes to voter
(define-public (return-votes-to-member (token <ft-trait>) (proposal-id uint) (member principal))
  (let (
    (token-count (get amount (get-tokens-by-member-by-id proposal-id member token)))
    (proposal (get-proposal-by-id proposal-id))
  )
    (asserts! (is-eq (is-token-accepted token) true) (err ERR-WRONG-TOKEN))
    (asserts! (is-eq (get is-open proposal) false) (err PROPOSAL-IS-NOT-OPEN))
    (asserts! (>= block-height (get end-block-height proposal)) (err ERR-NOT-AUTHORIZED))

    (as-contract (contract-call? token transfer token-count (as-contract tx-sender) member none))
  )
)

;; Make needed contract changes on DAO
(define-private (execute-proposal (proposal-id uint))
  (let (
    (proposal (get-proposal-by-id proposal-id))
    (contract-changes (get contract-changes proposal))
  )
    (if (> (len contract-changes) u0)
      (begin
        (map execute-proposal-change-contract contract-changes)
        (ok true)
      )
      (err ERR-NO-CONTRACT-CHANGES)
    )
  )
)

;; Helper to execute proposal and change contracts
(define-private (execute-proposal-change-contract (change (tuple (name (string-ascii 256)) (address principal) (qualified-name principal) (can-mint bool) (can-burn bool))))
  (let (
    (name (get name change))
    (address (get address change))
    (qualified-name (get qualified-name change))
    (can-mint (get can-mint change))
    (can-burn (get can-burn change))
  )
    (if (not (is-eq name ""))
      (begin
        (try! (contract-call? 'SP1Z92MPDQEWZXW36VX71Q25HKF5K2EPCJ304F275.stackswap-dao-v4a set-contract-address name address qualified-name can-mint can-burn))
        (ok true)
      )
      (ok false)
    )
  )
)

;; adds a new contract, only new ones allowed
(define-public (add-contract-address (name (string-ascii 256)) (address principal) (qualified-name principal) (can-mint bool) (can-burn bool))
  (begin
    (asserts! (is-eq tx-sender (contract-call? 'SP1Z92MPDQEWZXW36VX71Q25HKF5K2EPCJ304F275.stackswap-dao-v4a get-dao-owner)) (err ERR-NOT-AUTHORIZED))

    (if (is-some (contract-call? 'SP1Z92MPDQEWZXW36VX71Q25HKF5K2EPCJ304F275.stackswap-dao-v4a get-contract-address-by-name name))
      (ok false)
      (begin
        (try! (contract-call? 'SP1Z92MPDQEWZXW36VX71Q25HKF5K2EPCJ304F275.stackswap-dao-v4a set-contract-address name address qualified-name can-mint can-burn))
        (ok true)
      )
    )
  )
)
