(ns ajet.chat.shared.schema
  "Data schemas and validation for all entities using clojure.spec.alpha."
  (:require [clojure.spec.alpha :as s]
            [clojure.string :as str]))

;;; ---------------------------------------------------------------------------
;;; Predicates
;;; ---------------------------------------------------------------------------

(defn uuid-str?
  "Check if value is a valid UUID string."
  [v]
  (and (string? v)
       (re-matches #"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}" v)))

(defn uuid?
  "Check if value is a UUID (string or java.util.UUID)."
  [v]
  (or (instance? java.util.UUID v)
      (uuid-str? v)))

(defn slug?
  "Check if value is a valid slug: lowercase letters, digits, hyphens."
  [v]
  (and (string? v) (re-matches #"[a-z0-9][a-z0-9-]*[a-z0-9]" v) (<= (count v) 100)))

(defn non-blank-string?
  "Check if value is a non-blank string."
  [v]
  (and (string? v) (not (str/blank? v))))

;;; ---------------------------------------------------------------------------
;;; Common Specs
;;; ---------------------------------------------------------------------------

(s/def ::id uuid?)
(s/def ::uuid uuid?)
(s/def ::non-blank non-blank-string?)
(s/def ::slug slug?)
(s/def ::inst inst?)
(s/def ::optional-string (s/nilable string?))
(s/def ::optional-uuid (s/nilable uuid?))
(s/def ::optional-inst (s/nilable inst?))
(s/def ::positive-int (s/and int? pos?))

;;; ---------------------------------------------------------------------------
;;; Entity Specs
;;; ---------------------------------------------------------------------------

;; Community
(s/def :community/id ::uuid)
(s/def :community/name ::non-blank)
(s/def :community/slug ::slug)
(s/def :community/created-at ::inst)

(s/def ::community
  (s/keys :req-un [:community/id :community/name :community/slug]
          :opt-un [:community/created-at]))

;; User
(s/def :user/id ::uuid)
(s/def :user/username ::non-blank)
(s/def :user/display-name ::non-blank)
(s/def :user/email ::non-blank)
(s/def :user/avatar-url ::optional-string)
(s/def :user/status-text ::optional-string)
(s/def :user/created-at ::inst)

(s/def ::user
  (s/keys :req-un [:user/id :user/username :user/email]
          :opt-un [:user/display-name :user/avatar-url :user/status-text :user/created-at]))

;; OAuth Provider (DB-stored, runtime-configurable)
(s/def :oauth-provider/id ::uuid)
(s/def :oauth-provider/provider-type #{"github" "gitea" "oidc"})
(s/def :oauth-provider/display-name ::non-blank)
(s/def :oauth-provider/slug ::non-blank)
(s/def :oauth-provider/client-id ::non-blank)
(s/def :oauth-provider/client-secret ::non-blank)
(s/def :oauth-provider/base-url ::optional-string)
(s/def :oauth-provider/issuer-url ::optional-string)
(s/def :oauth-provider/enabled boolean?)
(s/def :oauth-provider/sort-order int?)

(s/def ::oauth-provider
  (s/keys :req-un [:oauth-provider/id :oauth-provider/provider-type
                   :oauth-provider/display-name :oauth-provider/slug
                   :oauth-provider/client-id :oauth-provider/client-secret]
          :opt-un [:oauth-provider/base-url :oauth-provider/issuer-url
                   :oauth-provider/enabled :oauth-provider/sort-order]))

;; OAuth Account
(s/def :oauth-account/id ::uuid)
(s/def :oauth-account/user-id ::uuid)
(s/def :oauth-account/provider #{"github" "gitea" "oidc"})
(s/def :oauth-account/provider-user-id ::non-blank)

(s/def ::oauth-account
  (s/keys :req-un [:oauth-account/id :oauth-account/user-id
                   :oauth-account/provider :oauth-account/provider-user-id]))

;; Session
(s/def :session/id ::uuid)
(s/def :session/user-id ::uuid)
(s/def :session/token-hash ::non-blank)
(s/def :session/expires-at ::inst)

(s/def ::session
  (s/keys :req-un [:session/id :session/user-id :session/token-hash :session/expires-at]))

;; Channel
(s/def :channel/id ::uuid)
(s/def :channel/community-id ::optional-uuid)
(s/def :channel/name ::non-blank)
(s/def :channel/type #{:text :voice :dm :group-dm})
(s/def :channel/visibility #{:public :private})
(s/def :channel/topic ::optional-string)
(s/def :channel/category-id ::optional-uuid)

(s/def ::channel
  (s/keys :req-un [:channel/id :channel/name :channel/type]
          :opt-un [:channel/community-id :channel/visibility :channel/topic
                   :channel/category-id]))

;; Channel Category
(s/def :category/id ::uuid)
(s/def :category/community-id ::uuid)
(s/def :category/name ::non-blank)
(s/def :category/position int?)

(s/def ::channel-category
  (s/keys :req-un [:category/id :category/community-id :category/name :category/position]))

;; Community Member
(s/def :community-member/community-id ::uuid)
(s/def :community-member/user-id ::uuid)
(s/def :community-member/role #{:owner :admin :member})
(s/def :community-member/nickname ::optional-string)

(s/def ::community-member
  (s/keys :req-un [:community-member/community-id :community-member/user-id
                   :community-member/role]))

;; Channel Member
(s/def :channel-member/channel-id ::uuid)
(s/def :channel-member/user-id ::uuid)
(s/def :channel-member/joined-at ::inst)
(s/def :channel-member/last-read-message-id ::optional-uuid)

(s/def ::channel-member
  (s/keys :req-un [:channel-member/channel-id :channel-member/user-id]
          :opt-un [:channel-member/joined-at :channel-member/last-read-message-id]))

;; Message
(s/def :message/id ::uuid)
(s/def :message/channel-id ::uuid)
(s/def :message/user-id ::uuid)
(s/def :message/parent-id ::optional-uuid)
(s/def :message/body-md ::non-blank)
(s/def :message/created-at ::inst)
(s/def :message/edited-at ::optional-inst)

(s/def ::message
  (s/keys :req-un [:message/id :message/channel-id :message/user-id :message/body-md]
          :opt-un [:message/parent-id :message/created-at :message/edited-at]))

;; Attachment
(s/def :attachment/id ::uuid)
(s/def :attachment/message-id ::uuid)
(s/def :attachment/filename ::non-blank)
(s/def :attachment/content-type #{"image/jpeg" "image/png" "image/gif" "image/webp"})
(s/def :attachment/size-bytes ::positive-int)
(s/def :attachment/storage-key ::non-blank)

(s/def ::attachment
  (s/keys :req-un [:attachment/id :attachment/message-id :attachment/filename
                   :attachment/content-type :attachment/size-bytes :attachment/storage-key]))

;; Reaction
(s/def :reaction/message-id ::uuid)
(s/def :reaction/user-id ::uuid)
(s/def :reaction/emoji ::non-blank)

(s/def ::reaction
  (s/keys :req-un [:reaction/message-id :reaction/user-id :reaction/emoji]))

;; Webhook
(s/def :webhook/id ::uuid)
(s/def :webhook/community-id ::uuid)
(s/def :webhook/channel-id ::uuid)
(s/def :webhook/name ::non-blank)
(s/def :webhook/token-hash ::non-blank)

(s/def ::webhook
  (s/keys :req-un [:webhook/id :webhook/community-id :webhook/channel-id
                   :webhook/name :webhook/token-hash]))

;; Mention
(s/def :mention/id ::uuid)
(s/def :mention/message-id ::uuid)
(s/def :mention/target-type #{:user :here :channel})
(s/def :mention/target-id ::optional-uuid)

(s/def ::mention
  (s/keys :req-un [:mention/id :mention/message-id :mention/target-type]
          :opt-un [:mention/target-id]))

;; Notification
(s/def :notification/id ::uuid)
(s/def :notification/user-id ::uuid)
(s/def :notification/type #{:mention :dm :thread-reply :invite})
(s/def :notification/source-id ::uuid)
(s/def :notification/read boolean?)

(s/def ::notification
  (s/keys :req-un [:notification/id :notification/user-id :notification/type
                   :notification/source-id :notification/read]))

;; Invite
(s/def :invite/id ::uuid)
(s/def :invite/community-id ::uuid)
(s/def :invite/created-by ::uuid)
(s/def :invite/code ::non-blank)
(s/def :invite/max-uses (s/nilable int?))
(s/def :invite/uses int?)
(s/def :invite/expires-at ::optional-inst)

(s/def ::invite
  (s/keys :req-un [:invite/id :invite/community-id :invite/created-by :invite/code :invite/uses]
          :opt-un [:invite/max-uses :invite/expires-at]))

;; API User
(s/def :api-user/id ::uuid)
(s/def :api-user/name ::non-blank)
(s/def :api-user/community-id ::uuid)

(s/def ::api-user
  (s/keys :req-un [:api-user/id :api-user/name :api-user/community-id]))

;; API Token
(s/def :api-token/id ::uuid)
(s/def :api-token/api-user-id ::uuid)
(s/def :api-token/token-hash ::non-blank)
(s/def :api-token/expires-at ::optional-inst)

(s/def ::api-token
  (s/keys :req-un [:api-token/id :api-token/api-user-id :api-token/token-hash]
          :opt-un [:api-token/expires-at]))

;;; ---------------------------------------------------------------------------
;;; Validation
;;; ---------------------------------------------------------------------------

(defn validate
  "Validate data against a spec. Returns data if valid, throws ex-info if not."
  [spec data]
  (if (s/valid? spec data)
    data
    (throw (ex-info "Validation failed"
                    {:type    :ajet.chat/validation-error
                     :spec    spec
                     :explain (s/explain-str spec data)}))))

(defn valid?
  "Check if data matches spec."
  [spec data]
  (s/valid? spec data))

;;; ---------------------------------------------------------------------------
;;; Request Validation Specs
;;; ---------------------------------------------------------------------------

(s/def :req/create-community
  (s/keys :req-un [:community/name :community/slug]))

(s/def :req/update-community
  (s/keys :opt-un [:community/name :community/slug]))

(s/def :req/create-channel
  (s/keys :req-un [:channel/name]
          :opt-un [:channel/type :channel/visibility :channel/topic :channel/category-id]))

(s/def :req/send-message
  (s/keys :req-un [:message/body-md]
          :opt-un [:message/parent-id]))

(s/def :req/edit-message
  (s/keys :req-un [:message/body-md]))

(s/def :req/create-dm
  (s/keys :req-un [::user-id]))

(s/def :req/create-group-dm
  (s/keys :req-un [::user-ids]))

(s/def :req/create-invite
  (s/keys :opt-un [:invite/max-uses ::expires-in-hours]))

(s/def :req/update-profile
  (s/keys :opt-un [:user/display-name :user/status-text]))
