;; NEWYORKCITYCOIN AUTH CONTRACT
;; CityCoins Protocol Version 1.0.1

(define-constant CONTRACT_OWNER tx-sender)

;; TRAIT DEFINITIONS

(use-trait coreTrait 'SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-core-trait.citycoin-core)
(use-trait tokenTrait 'SP466FNC0P7JWTNM2R9T199QRZN1MYEDTAR0KP27.citycoin-token-trait.citycoin-token)

;; ERRORS

(define-constant ERR_UNKNOWN_JOB u6000)
(define-constant ERR_UNAUTHORIZED u6001)
(define-constant ERR_JOB_IS_ACTIVE u6002)
(define-constant ERR_JOB_IS_NOT_ACTIVE u6003)
(define-constant ERR_ALREADY_VOTED_THIS_WAY u6004)
(define-constant ERR_JOB_IS_EXECUTED u6005)
(define-constant ERR_JOB_IS_NOT_APPROVED u6006)
(define-constant ERR_ARGUMENT_ALREADY_EXISTS u6007)
(define-constant ERR_NO_ACTIVE_CORE_CONTRACT u6008)
(define-constant ERR_CORE_CONTRACT_NOT_FOUND u6009)
(define-constant ERR_UNKNOWN_ARGUMENT u6010)

;; JOB MANAGEMENT

(define-constant REQUIRED_APPROVALS u3)

(define-data-var lastJobId uint u0)

(define-map Jobs
  uint
  {
    creator: principal,
    name: (string-ascii 255),
    target: principal,
    approvals: uint,
    disapprovals: uint,
    isActive: bool,
    isExecuted: bool
  }
)

(define-map JobApprovers
  { jobId: uint, approver: principal }
  bool
)

(define-map Approvers
  principal
  bool
)

(define-map ArgumentLastIdsByType
  { jobId: uint, argumentType: (string-ascii 25) }
  uint
)

(define-map UIntArgumentsByName
  { jobId: uint, argumentName: (string-ascii 255) }
  { argumentId: uint, value: uint}
)

(define-map UIntArgumentsById
  { jobId: uint, argumentId: uint }
  { argumentName: (string-ascii 255), value: uint }
)

(define-map PrincipalArgumentsByName
  { jobId: uint, argumentName: (string-ascii 255) }
  { argumentId: uint, value: principal }
)

(define-map PrincipalArgumentsById
  { jobId: uint, argumentId: uint }
  { argumentName: (string-ascii 255), value: principal }
)

;; FUNCTIONS

(define-read-only (get-last-job-id)
  (var-get lastJobId)
)

(define-public (create-job (name (string-ascii 255)) (target principal))
  (let
    (
      (newJobId (+ (var-get lastJobId) u1))
    )
    (asserts! (is-approver tx-sender) (err ERR_UNAUTHORIZED))
    (map-set Jobs
      newJobId
      {
        creator: tx-sender,
        name: name,
        target: target,
        approvals: u0,
        disapprovals: u0,
        isActive: false,
        isExecuted: false
      }
    )
    (var-set lastJobId newJobId)
    (ok newJobId)
  )
)

(define-read-only (get-job (jobId uint))
  (map-get? Jobs jobId)
)

(define-public (activate-job (jobId uint))
  (let
    (
      (job (unwrap! (get-job jobId) (err ERR_UNKNOWN_JOB)))
    )
    (asserts! (is-eq (get creator job) tx-sender) (err ERR_UNAUTHORIZED))
    (asserts! (not (get isActive job)) (err ERR_JOB_IS_ACTIVE))
    (map-set Jobs 
      jobId
      (merge job { isActive: true })
    )
    (ok true)
  )
)

(define-public (approve-job (jobId uint))
  (let
    (
      (job (unwrap! (get-job jobId) (err ERR_UNKNOWN_JOB)))
      (previousVote (map-get? JobApprovers { jobId: jobId, approver: tx-sender }))
    )
    (asserts! (get isActive job) (err ERR_JOB_IS_NOT_ACTIVE))
    (asserts! (is-approver tx-sender) (err ERR_UNAUTHORIZED))
    ;; save vote
    (map-set JobApprovers
      { jobId: jobId, approver: tx-sender }
      true
    )
    (match previousVote approved
      (begin
        (asserts! (not approved) (err ERR_ALREADY_VOTED_THIS_WAY))
        (map-set Jobs jobId
          (merge job 
            {
              approvals: (+ (get approvals job) u1),
              disapprovals: (- (get disapprovals job) u1)
            }
          )
        )
      )
      ;; no previous vote
      (map-set Jobs
        jobId
        (merge job { approvals: (+ (get approvals job) u1) } )
      )
    )  
    (ok true)
  )
)

(define-public (disapprove-job (jobId uint))
  (let
    (
      (job (unwrap! (get-job jobId) (err ERR_UNKNOWN_JOB)))
      (previousVote (map-get? JobApprovers { jobId: jobId, approver: tx-sender }))
    )
    (asserts! (get isActive job) (err ERR_JOB_IS_NOT_ACTIVE))
    (asserts! (is-approver tx-sender) (err ERR_UNAUTHORIZED))
    ;; save vote
    (map-set JobApprovers
      { jobId: jobId, approver: tx-sender }
      false
    )
    (match previousVote approved
      (begin
        (asserts! approved (err ERR_ALREADY_VOTED_THIS_WAY))
        (map-set Jobs jobId
          (merge job 
            {
              approvals: (- (get approvals job) u1),
              disapprovals: (+ (get disapprovals job) u1)
            }
          )
        )
      )
      ;; no previous vote
      (map-set Jobs
        jobId
        (merge job { disapprovals: (+ (get disapprovals job) u1) } )
      )
    )
    (ok true)
  )
)

(define-read-only (is-job-approved (jobId uint))
  (match (get-job jobId) job
    (>= (get approvals job) REQUIRED_APPROVALS)
    false
  )
)

(define-public (mark-job-as-executed (jobId uint))
  (let
    (
      (job (unwrap! (get-job jobId) (err ERR_UNKNOWN_JOB)))
    )
    (asserts! (get isActive job) (err ERR_JOB_IS_NOT_ACTIVE))
    (asserts! (>= (get approvals job) REQUIRED_APPROVALS) (err ERR_JOB_IS_NOT_APPROVED))
    (asserts! (is-eq (get target job) contract-caller) (err ERR_UNAUTHORIZED))
    (asserts! (not (get isExecuted job)) (err ERR_JOB_IS_EXECUTED))
    (map-set Jobs
      jobId
      (merge job { isExecuted: true })
    )
    (ok true)
  )
)

(define-public (add-uint-argument (jobId uint) (argumentName (string-ascii 255)) (value uint))
  (let
    (
      (argumentId (generate-argument-id jobId "uint"))
    )
    (try! (guard-add-argument jobId))
    (asserts! 
      (and
        (map-insert UIntArgumentsById
          { jobId: jobId, argumentId: argumentId }
          { argumentName: argumentName, value: value }
        )
        (map-insert UIntArgumentsByName
          { jobId: jobId, argumentName: argumentName }
          { argumentId: argumentId, value: value}
        )
      ) 
      (err ERR_ARGUMENT_ALREADY_EXISTS)
    )
    (ok true)
  )
)

(define-read-only (get-uint-argument-by-name (jobId uint) (argumentName (string-ascii 255)))
  (map-get? UIntArgumentsByName { jobId: jobId, argumentName: argumentName })
)

(define-read-only (get-uint-argument-by-id (jobId uint) (argumentId uint))
  (map-get? UIntArgumentsById { jobId: jobId, argumentId: argumentId })
)

(define-read-only (get-uint-value-by-name (jobId uint) (argumentName (string-ascii 255)))
  (get value (get-uint-argument-by-name jobId argumentName))
)

(define-read-only (get-uint-value-by-id (jobId uint) (argumentId uint))
  (get value (get-uint-argument-by-id jobId argumentId))
)

(define-public (add-principal-argument (jobId uint) (argumentName (string-ascii 255)) (value principal))
  (let
    (
      (argumentId (generate-argument-id jobId "principal"))
    )
    (try! (guard-add-argument jobId))
    (asserts! 
      (and
        (map-insert PrincipalArgumentsById
          { jobId: jobId, argumentId: argumentId }
          { argumentName: argumentName, value: value }
        )
        (map-insert PrincipalArgumentsByName
          { jobId: jobId, argumentName: argumentName }
          { argumentId: argumentId, value: value}
        )
      ) 
      (err ERR_ARGUMENT_ALREADY_EXISTS)
    )
    (ok true)
  )
)

(define-read-only (get-principal-argument-by-name (jobId uint) (argumentName (string-ascii 255)))
  (map-get? PrincipalArgumentsByName { jobId: jobId, argumentName: argumentName })
)

(define-read-only (get-principal-argument-by-id (jobId uint) (argumentId uint))
  (map-get? PrincipalArgumentsById { jobId: jobId, argumentId: argumentId })
)

(define-read-only (get-principal-value-by-name (jobId uint) (argumentName (string-ascii 255)))
  (get value (get-principal-argument-by-name jobId argumentName))
)

(define-read-only (get-principal-value-by-id (jobId uint) (argumentId uint))
  (get value (get-principal-argument-by-id jobId argumentId))
)

;; PRIVATE FUNCTIONS

(define-read-only  (is-approver (user principal))
  (default-to false (map-get? Approvers user))
)

(define-private (generate-argument-id (jobId uint) (argumentType (string-ascii 25)))
  (let
    (
      (argumentId (+ (default-to u0 (map-get? ArgumentLastIdsByType { jobId: jobId, argumentType: argumentType })) u1))
    )
    (map-set ArgumentLastIdsByType
      { jobId: jobId, argumentType: argumentType }
      argumentId
    )
    ;; return
    argumentId
  )
)

(define-private (guard-add-argument (jobId uint))
  (let
    (
      (job (unwrap! (get-job jobId) (err ERR_UNKNOWN_JOB)))
    )
    (asserts! (not (get isActive job)) (err ERR_JOB_IS_ACTIVE))
    (asserts! (is-eq (get creator job) contract-caller) (err ERR_UNAUTHORIZED))
    (ok true)
  )
)

;; CONTRACT MANAGEMENT

;; initial value for active core contract
;; set to deployer address at startup to prevent
;; circular dependency of core on auth
(define-data-var activeCoreContract principal CONTRACT_OWNER)
(define-data-var initialized bool false)

;; core contract states
(define-constant STATE_DEPLOYED u0)
(define-constant STATE_ACTIVE u1)
(define-constant STATE_INACTIVE u2)

;; core contract map
(define-map CoreContracts
  principal
  {
    state: uint, 
    startHeight: uint,
    endHeight: uint
  }
)

;; getter for active core contract
(define-read-only (get-active-core-contract)
  (begin
    (asserts! (not (is-eq (var-get activeCoreContract) CONTRACT_OWNER)) (err ERR_NO_ACTIVE_CORE_CONTRACT))
    (ok (var-get activeCoreContract))
  )
)

;; getter for core contract map
(define-read-only (get-core-contract-info (targetContract principal))
  (let
    (
      (coreContract (unwrap! (map-get? CoreContracts targetContract) (err ERR_CORE_CONTRACT_NOT_FOUND)))
    )
    (ok coreContract)
  )
)

;; one-time function to initialize contracts after all contracts are deployed
;; - check that deployer is calling this function
;; - check this contract is not activated already (one-time use)
;; - set initial map value for core contract v1
;; - set cityWallet in core contract
;; - set intialized true
(define-public (initialize-contracts (coreContract <coreTrait>))
  (let
    (
      (coreContractAddress (contract-of coreContract))
    )
    (asserts! (is-eq contract-caller CONTRACT_OWNER) (err ERR_UNAUTHORIZED))
    (asserts! (not (var-get initialized)) (err ERR_UNAUTHORIZED))
    (map-set CoreContracts
      coreContractAddress
      {
        state: STATE_DEPLOYED,
        startHeight: u0,
        endHeight: u0
      })
    (try! (contract-call? coreContract set-city-wallet (var-get cityWallet)))
    (var-set initialized true)
    (ok true)
  )
)

(define-read-only (is-initialized)
  (var-get initialized)
)

;; function to activate core contract through registration
;; - check that target is in core contract map
;; - check that caller is core contract
;; - set active in core contract map
;; - set as activeCoreContract
(define-public (activate-core-contract (targetContract principal) (stacksHeight uint))
  (let
    (
      (coreContract (unwrap! (map-get? CoreContracts targetContract) (err ERR_CORE_CONTRACT_NOT_FOUND)))
    )
    (asserts! (is-eq contract-caller targetContract) (err ERR_UNAUTHORIZED))
    (map-set CoreContracts
      targetContract
      {
        state: STATE_ACTIVE,
        startHeight: stacksHeight,
        endHeight: u0
      })
    (var-set activeCoreContract targetContract)
    (ok true)
  )
)

;; protected function to update core contract
(define-public (upgrade-core-contract (oldContract <coreTrait>) (newContract <coreTrait>))
  (let
    (
      (oldContractAddress (contract-of oldContract))
      (oldContractMap (unwrap! (map-get? CoreContracts oldContractAddress) (err ERR_CORE_CONTRACT_NOT_FOUND)))
      (newContractAddress (contract-of newContract))
    )
    (asserts! (not (is-eq oldContractAddress newContractAddress)) (err ERR_UNAUTHORIZED))
    (asserts! (is-authorized-city) (err ERR_UNAUTHORIZED))
    (map-set CoreContracts
      oldContractAddress
      {
        state: STATE_INACTIVE,
        startHeight: (get startHeight oldContractMap),
        endHeight: block-height
      })
    (map-set CoreContracts
      newContractAddress
      {
        state: STATE_DEPLOYED,
        startHeight: u0,
        endHeight: u0
      })
    (var-set activeCoreContract newContractAddress)
    (try! (contract-call? oldContract shutdown-contract block-height))
    (try! (contract-call? newContract set-city-wallet (var-get cityWallet)))
    (ok true)
  )
)

(define-public (execute-upgrade-core-contract-job (jobId uint) (oldContract <coreTrait>) (newContract <coreTrait>))
  (let
    (
      (oldContractArg (unwrap! (get-principal-value-by-name jobId "oldContract") (err ERR_UNKNOWN_ARGUMENT)))
      (newContractArg (unwrap! (get-principal-value-by-name jobId "newContract") (err ERR_UNKNOWN_ARGUMENT)))
      (oldContractAddress (contract-of oldContract))
      (oldContractMap (unwrap! (map-get? CoreContracts oldContractAddress) (err ERR_CORE_CONTRACT_NOT_FOUND)))
      (newContractAddress (contract-of newContract))
    )
    (asserts! (is-approver contract-caller) (err ERR_UNAUTHORIZED))
    (asserts! (and (is-eq oldContractArg oldContractAddress) (is-eq newContractArg newContractAddress)) (err ERR_UNAUTHORIZED))
    (asserts! (not (is-eq oldContractAddress newContractAddress)) (err ERR_UNAUTHORIZED))
    (map-set CoreContracts
      oldContractAddress
      {
        state: STATE_INACTIVE,
        startHeight: (get startHeight oldContractMap),
        endHeight: block-height
      })
    (map-set CoreContracts
      newContractAddress
      {
        state: STATE_DEPLOYED,
        startHeight: u0,
        endHeight: u0
      })
    (var-set activeCoreContract newContractAddress)
    (try! (contract-call? oldContract shutdown-contract block-height))
    (try! (contract-call? newContract set-city-wallet (var-get cityWallet)))
    (as-contract (mark-job-as-executed jobId))
  )
)

;; CITY WALLET MANAGEMENT

;; initial value for city wallet
(define-data-var cityWallet principal 'SM18VBF2QYAAHN57Q28E2HSM15F6078JZYZ2FQBCX)

;; returns city wallet principal
(define-read-only (get-city-wallet)
  (ok (var-get cityWallet))
)
 
;; protected function to update city wallet variable
(define-public (set-city-wallet (targetContract <coreTrait>) (newCityWallet principal))
  (let
    (
      (coreContractAddress (contract-of targetContract))
      (coreContract (unwrap! (map-get? CoreContracts coreContractAddress) (err ERR_CORE_CONTRACT_NOT_FOUND)))
    )
    (asserts! (is-authorized-city) (err ERR_UNAUTHORIZED))
    (asserts! (is-eq coreContractAddress (var-get activeCoreContract)) (err ERR_UNAUTHORIZED))
    (var-set cityWallet newCityWallet)
    (try! (contract-call? targetContract set-city-wallet newCityWallet))
    (ok true)
  )
)

(define-public (execute-set-city-wallet-job (jobId uint) (targetContract <coreTrait>))
  (let
    (
      (coreContractAddress (contract-of targetContract))
      (coreContract (unwrap! (map-get? CoreContracts coreContractAddress) (err ERR_CORE_CONTRACT_NOT_FOUND)))
      (newCityWallet (unwrap! (get-principal-value-by-name jobId "newCityWallet") (err ERR_UNKNOWN_ARGUMENT)))
    )
    (asserts! (is-approver contract-caller) (err ERR_UNAUTHORIZED))
    (asserts! (is-eq coreContractAddress (var-get activeCoreContract)) (err ERR_UNAUTHORIZED))
    (var-set cityWallet newCityWallet)
    (try! (contract-call? targetContract set-city-wallet newCityWallet))
    (as-contract (mark-job-as-executed jobId))
  )
)

;; check if contract caller is city wallet
(define-private (is-authorized-city)
  (is-eq contract-caller (var-get cityWallet))
)

;; TOKEN MANAGEMENT

(define-public (set-token-uri (targetContract <tokenTrait>) (newUri (optional (string-utf8 256))))
  (begin
    (asserts! (is-authorized-city) (err ERR_UNAUTHORIZED))
    (as-contract (try! (contract-call? targetContract set-token-uri newUri)))
    (ok true)
  )
)

;; APPROVERS MANAGEMENT

(define-public (execute-replace-approver-job (jobId uint))
  (let
    (
      (oldApprover (unwrap! (get-principal-value-by-name jobId "oldApprover") (err ERR_UNKNOWN_ARGUMENT)))
      (newApprover (unwrap! (get-principal-value-by-name jobId "newApprover") (err ERR_UNKNOWN_ARGUMENT)))
    )
    (asserts! (is-approver contract-caller) (err ERR_UNAUTHORIZED))
    (map-set Approvers oldApprover false)
    (map-set Approvers newApprover true)
    (as-contract (mark-job-as-executed jobId))
  )
)

;; CONTRACT INITIALIZATION

(map-insert Approvers 'SP372JVX6EWE2M0XPA84MWZYRRG2M6CAC4VVC12V1 true)
(map-insert Approvers 'SP2R0DQYR7XHD161SH2GK49QRP1YSV7HE9JSG7W6G true)
(map-insert Approvers 'SPN4Y5QPGQA8882ZXW90ADC2DHYXMSTN8VAR8C3X true)
(map-insert Approvers 'SP3YYGCGX1B62CYAH4QX7PQE63YXG7RDTXD8BQHJQ true)
(map-insert Approvers 'SP7DGES13508FHRWS1FB0J3SZA326FP6QRMB6JDE true)
