(ns ai.miniforge.connector-gitlab.impl
  "Implementation functions for the GitLab REST API connector.
   Small composable functions organized in a stratified DAG."
  (:require [ai.miniforge.connector.interface :as connector]
            [ai.miniforge.connector-gitlab.messages :as msg]
            [ai.miniforge.connector-gitlab.resources :as resources]
            [ai.miniforge.connector-gitlab.schema :as schema]
            [ai.miniforge.connector-http.interface :as http]
            [ai.miniforge.response.interface :as response])
  (:import [java.util UUID]))

;;------------------------------------------------------------------------------ Layer 0
;; Handle state

(def ^:private handles (connector/create-handle-registry))

(defn- store-handle! [handle state] (connector/store-handle! handles handle state))
(defn- remove-handle! [handle] (connector/remove-handle! handles handle))
(defn- touch-handle! [handle] (connector/touch-handle! handles handle))

(defn- require-handle!
  "Retrieve handle state or throw, delegating to the shared helper."
  [handle]
  (connector/require-handle! handles handle
                             {:message (msg/t :gitlab/handle-not-found {:handle handle})}))

;; Auth

(defn- build-auth-headers
  "Build authorization headers for GitLab API.
   Both :api-key and :oauth2 use Bearer — GitLab accepts Bearer for
   PATs, OAuth tokens, and project/group tokens."
  [{:auth/keys [method credential-id]}]
  (case method
    :api-key {"Authorization" (str "Bearer " credential-id)}
    :oauth2  {"Authorization" (str "Bearer " credential-id)}
    {}))

(defn- resolve-auth-headers
  "Build auth headers from auth config, defaulting to empty map."
  [auth]
  (if (:auth/method auth)
    (build-auth-headers auth)
    {}))

;; Rate limiting — response-header-driven via connector-http/rate-limit.
;; GitLab returns ratelimit-remaining and ratelimit-reset on every response.

(def ^:private gitlab-rate-headers
  {:remaining "ratelimit-remaining"
   :reset     "ratelimit-reset"
   :limit     "ratelimit-limit"})

(defn- update-rate-limits!
  "Parse rate limit headers from a response and store in handle state."
  [handle response-headers]
  (when-let [info (http/parse-rate-headers response-headers gitlab-rate-headers)]
    (http/update-rate-state! handles handle info)))

;; Config predicates

(defn- has-project?
  "Check if config identifies a GitLab project."
  [config]
  (some? (or (:gitlab/project-id config) (:gitlab/project-path config))))

(defn- validate-auth!
  "Validate auth credential reference, throwing on failure with the
   GitLab-localized message. Delegates to the shared
   `connector/validate-auth-or-throw!` helper so all the boundary
   throwers across connector-{jira,gitlab,github} read the same way."
  [auth]
  (connector/validate-auth-or-throw! auth msg/t :gitlab/auth-invalid))

;;------------------------------------------------------------------------------ Layer 1
;; HTTP

(defn- error-response
  [status resp]
  (http/error-response status resp
    {:rate-limited   (msg/t :gitlab/rate-limited)
     :request-failed (fn [s e] (msg/t :gitlab/request-failed {:status s :error e}))}))

(defn- do-request
  [url headers query-params]
  (http/do-request url headers query-params error-response))


(defn- response-records
  [resp]
  (http/coerce-records (:body resp)))

(defn- final-cursor
  [resource-def records]
  (http/last-record-cursor resource-def records))

(defn- page-fetch-result
  [records cursor]
  {:records records
   :cursor  cursor})

(def ^:private note-parent-scopes
  [{:resource-key   :issues
    :noteable-kind  "issues"
    :noteable-type  "Issue"}
   {:resource-key   :merge-requests
    :noteable-kind  "merge_requests"
    :noteable-type  "MergeRequest"}])

(defn- fetch-all-pages
  [handle resource-def url headers query-params]
  (loop [next-request-url url
         next-query-params query-params
         accumulated []]
    (http/acquire-permit! handles handle {})
    (let [resp (http/throw-on-failure! (do-request next-request-url headers next-query-params))]
      (update-rate-limits! handle (:headers resp))
      (touch-handle! handle)
      (if (:not-modified resp)
        (page-fetch-result [] nil)
        (let [page-records (response-records resp)
              all-records  (into accumulated page-records)]
          (if-let [next-link (http/next-url resp)]
            (recur next-link nil all-records)
            (page-fetch-result all-records
                               (final-cursor resource-def all-records))))))))

(defn- timestamp-value
  [record]
  (or (:updated_at record)
      (:created_at record)))

;; Resource lookup

(defn- require-resource!
  "Look up resource def or throw."
  [schema-name]
  (or (resources/get-resource (keyword schema-name))
      (response/throw-anomaly! :anomalies/not-found
                               (msg/t :gitlab/resource-unknown {:resource schema-name})
                               {:resource schema-name})))

(defn- resource-records
  [handle resource-def config auth-headers cursor opts]
  (let [url    (resources/build-url (:gitlab/base-url config) resource-def config)
        params (resources/build-query-params resource-def cursor opts)]
    (:records (fetch-all-pages handle resource-def url auth-headers params))))

(defn- note-parent-records
  [handle config auth-headers cursor opts]
  (letfn [(scope->parent-pairs [{:keys [resource-key] :as scope}]
            (let [resource-def (require-resource! (name resource-key))
                  records      (resource-records handle resource-def config auth-headers cursor opts)]
              (map #(vector scope %) records)))]
    (mapcat scope->parent-pairs note-parent-scopes)))

(defn- enrich-note
  [scope parent note]
  (assoc note
         :project_id (or (:project_id note) (:project_id parent))
         :noteable_type (or (:noteable_type note) (:noteable-type scope))
         :noteable_kind (:noteable-kind scope)
         :noteable_iid (:iid parent)
         :noteable_id (:id parent)
         :noteable_title (:title parent)
         :noteable_web_url (:web_url parent)
         :noteable_state (:state parent)))

(defn- parent-note-records
  [handle notes-resource config auth-headers cursor opts [scope parent]]
  (let [note-config (assoc config
                           :gitlab/noteable-kind (:noteable-kind scope)
                           :gitlab/noteable-iid (:iid parent))
        records     (resource-records handle notes-resource note-config auth-headers nil opts)]
    (->> records
         (map #(enrich-note scope parent %))
         (filter #(http/after-cursor? timestamp-value cursor %)))))

(defn- extract-project-notes
  [handle config auth-headers cursor opts]
  (let [notes-resource (require-resource! "notes")
        notes         (->> (note-parent-records handle config auth-headers cursor opts)
                           (mapcat #(parent-note-records handle notes-resource config auth-headers cursor opts %))
                           (http/sort-by-timestamp timestamp-value)
                           vec)]
    (connector/extract-result notes (http/max-timestamp-cursor timestamp-value notes) false)))

;;------------------------------------------------------------------------------ Layer 2
;; Lifecycle and source operations

(defn do-connect
  "Validate config at boundary, register handle."
  [config auth]
  (let [config (assoc config :gitlab/base-url
                      (get config :gitlab/base-url "https://gitlab.com"))]
    (when-not (has-project? config)
      (response/throw-anomaly! :anomalies/incorrect
                               (msg/t :gitlab/project-required)
                               {:config config}))
    (schema/validate! schema/GitLabConfig config)
    (validate-auth! auth)
    (let [handle (str (UUID/randomUUID))]
      (store-handle! handle {:config       config
                              :auth-headers (resolve-auth-headers auth)
                              :last-request-at nil})
      (connector/connect-result handle))))

(defn do-close [handle]
  (remove-handle! handle)
  (connector/close-result))

(defn do-discover [handle]
  (require-handle! handle)
  (connector/discover-result (resources/resource-schemas)))

(defn do-extract
  "Extract records from a GitLab API resource."
  [handle schema-name opts]
  (let [handle-state (require-handle! handle)
        resource-def (require-resource! schema-name)
        {:keys [config auth-headers]} handle-state]
    (if (= "notes" schema-name)
      (extract-project-notes handle config auth-headers (:extract/cursor opts) opts)
      (let [url     (resources/build-url (:gitlab/base-url config) resource-def config)
            params  (resources/build-query-params resource-def (:extract/cursor opts) opts)
            fetch   (fn [] (let [{:keys [records cursor]}
                                 (fetch-all-pages handle resource-def url auth-headers params)]
                             (connector/extract-result records cursor false)))]
        (if (:optional? resource-def)
          (try (fetch)
               (catch clojure.lang.ExceptionInfo e
                 (when-not (= :permanent (:error-type (ex-data e))) (throw e))
                 (connector/extract-result [] nil false)))
          (fetch))))))

(defn do-checkpoint [cursor-state]
  (connector/checkpoint-result cursor-state))

(comment
  ;; (do-connect {:gitlab/project-path "engrammicai/ixi-services/services/ixi"} nil)
  )
