(ns lazygitclj.core
  "lazygitclj - A lazygit-inspired TUI for git."
  (:require [tui.core :as tui :refer [quit]]
            [lazygitclj.git :as git]
            [clojure.string :as str]))

;; === Model ===

(defn load-git-data []
  {:branch (git/current-branch)
   :sha (git/head-sha)
   :ahead-behind (git/ahead-behind)
   :staged (git/staged-files)
   :unstaged (git/unstaged-files)
   :commits (git/recent-commits 15)
   :branches (git/branches)
   :remote-branches (git/remote-branches)
   :tags (git/list-tags)
   :stashes (git/stash-list)
   :reflog (git/reflog 50)
   :diff nil})

;; initial-model is defined after helpers/update functions (needs update-diff)

;; === Helpers ===

(defn file-items [model]
  (let [staged (mapv #(assoc % :type :staged) (:staged model))
        unstaged (mapv #(assoc % :type :unstaged) (:unstaged model))]
    (vec (concat staged unstaged))))

(defn current-items [model]
  (case (:panel model)
    :files (file-items model)
    :commits (if (= (:commits-tab model) :reflog)
               (:reflog model)
               (:commits model))
    :branches (case (:branches-tab model)
                :local (:branches model)
                :remotes (:remote-branches model)
                :tags (:tags model)
                (:branches model))
    :stash (:stashes model)
    []))

(defn clamp-cursor [model]
  (let [items (current-items model)
        max-idx (max 0 (dec (count items)))]
    (update model :cursor #(min max-idx (max 0 %)))))

(defn truncate [s max-len]
  (if (and s (> (count s) max-len))
    (str (subs s 0 max-len) "...")
    s))

(defn visible-window
  "Calculate the visible window of items for scrolling.
   Returns {:items visible-items :start-idx start-index :total total-count}
   Keeps cursor visible by scrolling the window."
  [items cursor max-visible]
  (let [total (count items)
        max-visible (max 1 max-visible)]
    (if (<= total max-visible)
      ;; All items fit, no scrolling needed
      {:items items :start-idx 0 :total total}
      ;; Need to scroll - calculate window that keeps cursor visible
      (let [;; Simple approach: cursor should be in the middle when possible
            half (quot max-visible 2)
            ;; Calculate start index
            start-idx (cond
                        ;; Cursor near start - show from beginning
                        (<= cursor half) 0
                        ;; Cursor near end - show end portion
                        (>= cursor (- total half)) (- total max-visible)
                        ;; Cursor in middle - center it
                        :else (- cursor half))
            start-idx (max 0 (min start-idx (- total max-visible)))
            visible-items (subvec (vec items) start-idx (+ start-idx max-visible))]
        {:items visible-items :start-idx start-idx :total total}))))

(defn get-current-diff
  "Get the diff for the currently selected item based on panel and cursor."
  [model]
  (let [panel (:panel model)
        items (current-items model)
        cursor (:cursor model)
        item (get (vec items) cursor)]
    (case panel
      :files
      (when item
        (if (= (:type item) :staged)
          (git/diff-staged-file (:path item))
          (git/diff-unstaged (:path item))))

      :commits
      (when-let [sha (:sha item)]
        (git/show-commit sha))

      :branches
      (when item
        (case (:branches-tab model)
          :local (git/diff-branch item)
          :remotes nil
          :tags nil
          nil))

      :stash
      (when-let [ref (:ref item)]
        (git/stash-show ref))

      nil)))

;; === Update ===

(defn update-diff [model]
  (assoc model :diff (get-current-diff model)))

(defn initial-model []
  (let [base (merge
              {:panel :files
               :cursor 0
               :message nil
               :input-mode nil
               :input-buffer ""
               :input-context nil
               :menu-mode nil
               :commits-tab :commits
               :branches-tab :local
               :reflog-index 0}
              (load-git-data))]
    (update-diff base)))

(defn refresh [model]
  (-> model
      (merge (load-git-data))
      clamp-cursor
      (assoc :message nil)
      update-diff))

(defn update-files [model msg]
  (let [items (file-items model)
        cursor (:cursor model)
        item (get items cursor)]
    (cond
      (tui/key= msg " ")
      (if item
        (do
          (if (= (:type item) :staged)
            (git/unstage-file (:path item))
            (git/stage-file (:path item)))
          [(refresh model) nil])
        [model nil])

      (tui/key= msg "a")
      (if (and (seq (:staged model)) (empty? (:unstaged model)))
        ;; All files are staged - unstage all
        (do
          (git/reset-staged)
          [(refresh model) nil])
        ;; Some files are unstaged - stage all
        (do
          (git/stage-all)
          [(refresh model) nil]))

      (tui/key= msg "d")
      (if (and item (= (:type item) :unstaged) (not= (:index item) \?))
        (do
          (git/discard-file (:path item))
          [(refresh model) nil])
        [(assoc model :message "Can only discard unstaged changes") nil])

      ;; s: Quick stash all changes
      (tui/key= msg "s")
      (do
        (git/stash-all)
        [(-> model refresh (assoc :message "Stashed all changes")) nil])

      ;; S: Stash options menu
      (tui/key= msg "S")
      [(assoc model :menu-mode :stash-options) nil]

      :else
      [model nil])))

(defn update-commits [model msg]
  (let [items (current-items model)
        cursor (:cursor model)
        item (get (vec items) cursor)
        sha (when item (:sha item))]
    (cond
      ;; Space: Checkout commit/reflog entry
      (tui/key= msg " ")
      (if sha
        (do
          (git/checkout-reflog-entry sha)
          [(-> model refresh (assoc :message (str "Checked out " sha))) nil])
        [model nil])

      ;; g: Reset to this commit
      (tui/key= msg "g")
      (if sha
        (do
          (git/reset-to-reflog sha)
          [(-> model refresh (assoc :message (str "Reset to " sha))) nil])
        [model nil])

      ;; C: Cherry-pick commit
      (tui/key= msg "C")
      (if sha
        (do
          (git/cherry-pick-commit sha)
          [(-> model refresh (assoc :message (str "Cherry-picked " sha))) nil])
        [model nil])

      ;; t: Revert commit
      (tui/key= msg "t")
      (if sha
        (do
          (git/revert-commit sha)
          [(-> model refresh (assoc :message (str "Reverted " sha))) nil])
        [model nil])

      ;; r: Reword commit (HEAD only)
      (tui/key= msg "r")
      (if sha
        [(assoc model :input-mode :reword :input-buffer "" :input-context sha) nil]
        [model nil])

      ;; y: Copy SHA
      (tui/key= msg "y")
      (if sha
        [(assoc model :message (str "SHA: " sha " (not copied - no clipboard support)")) nil]
        [model nil])

      :else
      [model nil])))

(defn update-branches [model msg]
  (let [items (current-items model)
        cursor (:cursor model)
        item (get (vec items) cursor)
        tab (:branches-tab model)]
    (cond
      ;; Enter: checkout branch/tag
      (tui/key= msg :enter)
      (cond
        (and (= tab :local) item (not= item (:branch model)))
        (do
          (git/checkout-branch item)
          [(-> model refresh (assoc :message (str "Checked out " item))) nil])

        (and (= tab :tags) item)
        (do
          (git/checkout-tag item)
          [(-> model refresh (assoc :message (str "Checked out tag " item))) nil])

        :else [model nil])

      ;; n: new branch (local tab only)
      (and (tui/key= msg "n") (= tab :local))
      [(assoc model :input-mode :new-branch :input-buffer "") nil]

      ;; d: delete branch (local tab only)
      (and (tui/key= msg "d") (= tab :local) item (not= item (:branch model)))
      (do
        (git/delete-branch item)
        [(-> model refresh (assoc :message (str "Deleted branch " item))) nil])

      ;; R: rename branch (local tab only)
      (and (tui/key= msg "R") (= tab :local) item)
      [(assoc model :input-mode :rename-branch :input-buffer "" :input-context item) nil]

      ;; M: merge branch into current
      (and (tui/key= msg "M") (= tab :local) item (not= item (:branch model)))
      (do
        (git/merge-branch item)
        [(-> model refresh (assoc :message (str "Merged " item))) nil])

      ;; f: fast-forward branch
      (and (tui/key= msg "f") (= tab :local) item)
      (do
        (git/fast-forward-branch item)
        [(-> model refresh (assoc :message (str "Fast-forwarded " item))) nil])

      ;; Tab switching with [ and ]
      (tui/key= msg "[")
      [(-> model
           (update :branches-tab #(case % :local :tags :remotes :local :tags :remotes))
           (assoc :cursor 0)
           clamp-cursor) nil]

      (tui/key= msg "]")
      [(-> model
           (update :branches-tab #(case % :local :remotes :remotes :tags :tags :local))
           (assoc :cursor 0)
           clamp-cursor) nil]

      :else
      [model nil])))

(defn update-stash [model msg]
  (let [stashes (:stashes model)
        cursor (:cursor model)
        stash (get (vec stashes) cursor)
        ref (when stash (:ref stash))]
    (cond
      ;; Space: apply stash (keep in list)
      (tui/key= msg " ")
      (if ref
        (do
          (git/stash-apply ref)
          [(-> model refresh (assoc :message (str "Applied " ref))) nil])
        [model nil])

      ;; g: pop stash (apply and remove)
      (tui/key= msg "g")
      (if ref
        (do
          (git/stash-pop ref)
          [(-> model refresh (assoc :message (str "Popped " ref))) nil])
        [model nil])

      ;; d: drop stash
      (tui/key= msg "d")
      (if ref
        (do
          (git/stash-drop ref)
          [(-> model refresh (assoc :message (str "Dropped " ref))) nil])
        [model nil])

      ;; n: new branch from stash
      (and (tui/key= msg "n") ref)
      [(assoc model :input-mode :stash-branch :input-buffer "" :input-context ref) nil]

      :else
      [model nil])))

(defn update-input-mode [model msg]
  (cond
    (tui/key= msg :escape)
    [(assoc model :input-mode nil :input-buffer "" :input-context nil) nil]

    (tui/key= msg :enter)
    (let [buf (:input-buffer model)
          ctx (:input-context model)]
      (case (:input-mode model)
        :commit
        (if (str/blank? buf)
          [(assoc model :message "Commit message cannot be empty") nil]
          (do
            (git/commit buf)
            [(-> model
                 (assoc :input-mode nil :input-buffer "" :input-context nil)
                 refresh
                 (assoc :message "Committed!")) nil]))

        :new-branch
        (if (str/blank? buf)
          [(assoc model :message "Branch name cannot be empty") nil]
          (do
            (git/create-branch buf)
            [(-> model
                 (assoc :input-mode nil :input-buffer "" :input-context nil)
                 refresh
                 (assoc :message (str "Created branch " buf))) nil]))

        :rename-branch
        (if (str/blank? buf)
          [(assoc model :message "New name cannot be empty") nil]
          (do
            (git/rename-branch ctx buf)
            [(-> model
                 (assoc :input-mode nil :input-buffer "" :input-context nil)
                 refresh
                 (assoc :message (str "Renamed " ctx " to " buf))) nil]))

        :stash-branch
        (if (str/blank? buf)
          [(assoc model :message "Branch name cannot be empty") nil]
          (do
            (git/stash-branch buf ctx)
            [(-> model
                 (assoc :input-mode nil :input-buffer "" :input-context nil)
                 refresh
                 (assoc :message (str "Created branch " buf " from stash"))) nil]))

        :reword
        (if (str/blank? buf)
          [(assoc model :message "Message cannot be empty") nil]
          (if (git/reword-commit ctx buf)
            [(-> model
                 (assoc :input-mode nil :input-buffer "" :input-context nil)
                 refresh
                 (assoc :message "Rewrote commit message")) nil]
            [(assoc model :input-mode nil :input-buffer "" :input-context nil
                    :message "Can only reword HEAD commit") nil]))

        [model nil]))

    (tui/key= msg :backspace)
    [(update model :input-buffer #(if (empty? %) % (subs % 0 (dec (count %))))) nil]

    ;; Character input - key format is [:key {:char \a}]
    (and (= (first msg) :key)
         (map? (second msg))
         (:char (second msg))
         (not (:ctrl (second msg)))
         (not (:alt (second msg))))
    [(update model :input-buffer str (:char (second msg))) nil]

    :else
    [model nil]))

(defn update-stash-menu [model msg]
  (cond
    (tui/key= msg :escape)
    [(assoc model :menu-mode nil) nil]

    ;; a: Stash all changes
    (tui/key= msg "a")
    (do
      (git/stash-all)
      [(-> model (assoc :menu-mode nil) refresh (assoc :message "Stashed all changes")) nil])

    ;; i: Stash all changes and keep index
    (tui/key= msg "i")
    (do
      (git/stash-keep-index)
      [(-> model (assoc :menu-mode nil) refresh (assoc :message "Stashed all changes (kept index)")) nil])

    ;; U: Stash all including untracked files
    (tui/key= msg "U")
    (do
      (git/stash-include-untracked)
      [(-> model (assoc :menu-mode nil) refresh (assoc :message "Stashed all changes (including untracked)")) nil])

    ;; s: Stash staged changes only
    (tui/key= msg "s")
    (do
      (git/stash-staged)
      [(-> model (assoc :menu-mode nil) refresh (assoc :message "Stashed staged changes only")) nil])

    ;; u: Stash unstaged changes only
    (tui/key= msg "u")
    (do
      (git/stash-unstaged)
      [(-> model (assoc :menu-mode nil) refresh (assoc :message "Stashed unstaged changes only")) nil])

    :else
    [model nil]))

(defn update-reset-menu [model msg]
  (cond
    (tui/key= msg :escape)
    [(assoc model :menu-mode nil) nil]

    ;; s: Soft reset (keep staged)
    (tui/key= msg "s")
    (do
      (git/reset-soft)
      [(-> model (assoc :menu-mode nil) refresh (assoc :message "Soft reset to HEAD~1")) nil])

    ;; m: Mixed reset (unstage)
    (tui/key= msg "m")
    (do
      (git/reset-mixed)
      [(-> model (assoc :menu-mode nil) refresh (assoc :message "Mixed reset to HEAD~1")) nil])

    ;; h: Hard reset (discard all)
    (tui/key= msg "h")
    (do
      (git/reset-hard)
      [(-> model (assoc :menu-mode nil) refresh (assoc :message "Hard reset to HEAD~1")) nil])

    ;; u: Unstage all
    (tui/key= msg "u")
    (do
      (git/reset-staged)
      [(-> model (assoc :menu-mode nil) refresh (assoc :message "Unstaged all changes")) nil])

    ;; d: Discard unstaged changes
    (tui/key= msg "d")
    (do
      (git/discard-all-unstaged)
      [(-> model (assoc :menu-mode nil) refresh (assoc :message "Discarded unstaged changes")) nil])

    ;; c: Clean untracked files
    (tui/key= msg "c")
    (do
      (git/clean-untracked)
      [(-> model (assoc :menu-mode nil) refresh (assoc :message "Cleaned untracked files")) nil])

    :else
    [model nil]))

(defn update-help [model msg]
  (cond
    (or (tui/key= msg :escape) (tui/key= msg "?") (tui/key= msg "q"))
    [(assoc model :menu-mode nil) nil]

    :else
    [model nil]))

(defn update-model [model msg]
  (cond
    ;; Menu modes take priority
    (= (:menu-mode model) :stash-options)
    (update-stash-menu model msg)

    (= (:menu-mode model) :reset-options)
    (update-reset-menu model msg)

    (= (:menu-mode model) :help)
    (update-help model msg)

    (:input-mode model)
    (update-input-mode model msg)

    (or (tui/key= msg "q") (tui/key= msg [:ctrl \c]))
    [model quit]

    (tui/key= msg "r")
    [(refresh model) nil]

    ;; Help panel
    (tui/key= msg "?")
    [(assoc model :menu-mode :help) nil]

    ;; Reset options menu
    (tui/key= msg "D")
    [(assoc model :menu-mode :reset-options) nil]

    ;; Panel jump keys (matching lazygit: 2=Files, 3=Branches, 4=Commits, 5=Stash)
    (tui/key= msg "2")
    [(-> model (assoc :panel :files :cursor 0) clamp-cursor update-diff) nil]

    (tui/key= msg "3")
    [(-> model (assoc :panel :branches :cursor 0) clamp-cursor update-diff) nil]

    (tui/key= msg "4")
    [(-> model (assoc :panel :commits :cursor 0) clamp-cursor update-diff) nil]

    (tui/key= msg "5")
    [(-> model (assoc :panel :stash :cursor 0) clamp-cursor update-diff) nil]

    ;; h/l or Left/Right: move between panels (order: files → branches → commits → stash)
    (or (tui/key= msg "h") (tui/key= msg :left))
    [(-> model
         (update :panel #(case % :files :stash :branches :files :commits :branches :stash :commits))
         (assoc :cursor 0)
         clamp-cursor
         update-diff) nil]

    (or (tui/key= msg "l") (tui/key= msg :right))
    [(-> model
         (update :panel #(case % :files :branches :branches :commits :commits :stash :stash :files))
         (assoc :cursor 0)
         clamp-cursor
         update-diff) nil]

    (or (tui/key= msg :up) (tui/key= msg "k"))
    [(-> model (update :cursor dec) clamp-cursor update-diff) nil]

    (or (tui/key= msg :down) (tui/key= msg "j"))
    [(-> model (update :cursor inc) clamp-cursor update-diff) nil]

    (and (tui/key= msg "c") (= (:panel model) :files))
    (if (empty? (:staged model))
      [(assoc model :message "Nothing staged to commit") nil]
      [(assoc model :input-mode :commit :input-buffer "") nil])

    (tui/key= msg "P")
    (do
      (git/push)
      [(assoc model :message "Pushed!") nil])

    (tui/key= msg "p")
    (do
      (git/pull)
      [(-> model refresh (assoc :message "Pulled!")) nil])

    ;; Tab switching with [ and ] (for commits panel)
    (and (tui/key= msg "[") (= (:panel model) :commits))
    [(-> model
         (update :commits-tab #(if (= % :reflog) :commits :reflog))
         (assoc :cursor 0)
         clamp-cursor) nil]

    (and (tui/key= msg "]") (= (:panel model) :commits))
    [(-> model
         (update :commits-tab #(if (= % :commits) :reflog :commits))
         (assoc :cursor 0)
         clamp-cursor) nil]

    ;; Global undo (z): Reset to previous reflog entry
    (tui/key= msg "z")
    (let [reflog (:reflog model)
          current-idx (or (:reflog-index model) 0)
          next-idx (inc current-idx)]
      (if (< next-idx (count reflog))
        (let [entry (get (vec reflog) next-idx)]
          (git/reset-to-reflog (:ref entry))
          [(-> model
               refresh
               (assoc :reflog-index next-idx)
               (assoc :message (str "Undo: reset to " (:ref entry)))) nil])
        [(assoc model :message "Nothing to undo") nil]))

    ;; Global redo (Z): Reset forward in reflog
    (tui/key= msg "Z")
    (let [current-idx (or (:reflog-index model) 0)]
      (if (> current-idx 0)
        (let [prev-idx (dec current-idx)
              reflog (:reflog model)
              entry (get (vec reflog) prev-idx)]
          (git/reset-to-reflog (:ref entry))
          [(-> model
               refresh
               (assoc :reflog-index prev-idx)
               (assoc :message (str "Redo: reset to " (:ref entry)))) nil])
        [(assoc model :message "Nothing to redo") nil]))

    :else
    (case (:panel model)
      :files (update-files model msg)
      :commits (update-commits model msg)
      :branches (update-branches model msg)
      :stash (update-stash model msg)
      [model nil])))

;; === Views ===

(defn file-status-char [{:keys [index worktree]}]
  (str index worktree))

;; Panel 1: Status
(defn status-panel [{:keys [branch sha ahead-behind panel]}]
  (let [[ahead behind] ahead-behind
        sync-info (when (and ahead behind (or (pos? ahead) (pos? behind)))
                    (str "↑" ahead "↓" behind))]
    [:box {:border (if (= panel :status) :double :single)
           :title "1 Status" :padding [0 1] :width :fill :height :fill}
     (into [:row {:gap 1}]
           (remove nil?
                   [[:text {:fg :magenta :bold true} (truncate (or branch "?") 12)]
                    [:text "→"]
                    [:text {:fg :yellow} (or sha "?")]
                    (when sync-info [:text {:fg :cyan} sync-info])]))]))

;; Panel 2: Files
(defn files-panel [{:keys [staged unstaged cursor panel]} max-visible]
  (let [active? (= panel :files)
        all-files (into (mapv #(assoc % :section :staged) staged)
                        (mapv #(assoc % :section :unstaged) unstaged))
        {:keys [items start-idx total]} (visible-window all-files (if active? cursor 0) max-visible)]
    [:box {:border (if active? :double :single)
           :title (str "2 Files (" total ")")
           :padding [0 1] :width :fill :height :fill}
     (if (empty? all-files)
       [:text {:fg :gray} "No changes"]
       (into [:col]
             (for [[local-idx file] (map-indexed vector items)]
               (let [global-idx (+ start-idx local-idx)
                     is-cursor (and active? (= global-idx cursor))
                     color (if (= (:section file) :staged) :green :red)]
                 [:row {:gap 1}
                  [:text {:fg color} (file-status-char file)]
                  [:text {:fg (if is-cursor :cyan color) :bold is-cursor :inverse is-cursor}
                   (truncate (:path file) 20)]]))))]))

;; Panel 3: Branches
(defn branches-panel [{:keys [branches remote-branches tags branch branches-tab cursor panel]} max-visible]
  (let [active? (= panel :branches)
        all-items (case branches-tab :local branches :remotes remote-branches :tags tags branches)
        {:keys [items start-idx total]} (visible-window all-items (if active? cursor 0) max-visible)
        tab-str (case branches-tab :local "[L] R T" :remotes "L [R] T" :tags "L R [T]")]
    [:box {:border (if active? :double :single)
           :title (str "3 Branches " tab-str)
           :padding [0 1] :width :fill :height :fill}
     (if (empty? all-items)
       [:text {:fg :gray} (str "No " (name branches-tab))]
       (into [:col]
             (for [[local-idx item] (map-indexed vector items)]
               (let [global-idx (+ start-idx local-idx)
                     is-cursor (and active? (= global-idx cursor))
                     is-current (and (= branches-tab :local) (= item branch))
                     fg (cond is-current :green is-cursor :cyan :else :white)]
                 [:text {:fg fg :bold (or is-current is-cursor) :inverse is-cursor}
                  (str (if is-current "* " "  ") (truncate item 22))]))))]))

;; Panel 4: Commits
(defn commits-panel [{:keys [commits reflog commits-tab cursor panel]} max-visible]
  (let [active? (= panel :commits)
        reflog? (= commits-tab :reflog)
        all-items (if reflog? reflog commits)
        {:keys [items start-idx total]} (visible-window all-items (if active? cursor 0) max-visible)]
    [:box {:border (if active? :double :single)
           :title (str "4 Commits " (if reflog? "C [R]" "[C] R"))
           :padding [0 1] :width :fill :height :fill}
     (if (empty? all-items)
       [:text {:fg :gray} "No commits"]
       (into [:col]
             (for [[local-idx {:keys [sha action subject]}] (map-indexed vector items)]
               (let [global-idx (+ start-idx local-idx)
                     is-cursor (and active? (= global-idx cursor))]
                 [:row {:gap 1}
                  [:text {:fg :yellow} (or sha "?")]
                  [:text {:fg (if is-cursor :cyan :white) :bold is-cursor :inverse is-cursor}
                   (truncate (if reflog? (str action ": " subject) (or subject "")) 14)]]))))]))

;; Panel 5: Stash
(defn stash-panel [{:keys [stashes cursor panel]} max-visible]
  (let [active? (= panel :stash)
        {:keys [items start-idx total]} (visible-window stashes (if active? cursor 0) max-visible)]
    [:box {:border (if active? :double :single)
           :title (str "5 Stash (" total ")")
           :padding [0 1] :width :fill :height :fill}
     (if (empty? stashes)
       [:text {:fg :gray} "No stashes"]
       (into [:col]
             (for [[local-idx {:keys [index message]}] (map-indexed vector items)]
               (let [global-idx (+ start-idx local-idx)
                     is-cursor (and active? (= global-idx cursor))]
                 [:row {:gap 1}
                  [:text {:fg :yellow} (str (or index global-idx))]
                  [:text {:fg (if is-cursor :cyan :white) :bold is-cursor :inverse is-cursor}
                   (truncate (or message "") 20)]]))))]))

;; Panel 0: Main View (Diff)
(defn main-view-panel [{:keys [diff]}]
  (let [lines (when diff (str/split-lines diff))]
    [:box {:border :single :title "0 Main" :padding [0 1] :width :fill :height :fill}
     (if (empty? lines)
       [:text {:fg :gray} "Select an item to view diff"]
       [:col
        (for [line lines]
          [:text {:fg (cond
                        (str/starts-with? line "+") :green
                        (str/starts-with? line "-") :red
                        (str/starts-with? line "@@") :cyan
                        (or (str/starts-with? line "diff")
                            (str/starts-with? line "commit")) :yellow
                        :else :white)}
           line])])]))

;; Command Log (placeholder)
(defn command-log-panel []
  [:box {:border :single :title "Command Log" :padding [0 1] :width :fill :height :fill}
   [:text {:fg :gray} ""]])

;; Bottom help bar
(defn help-bar [model]
  (let [panel (:panel model)
        panel-help (case panel
                     :files "spc:stage a:all c:commit"
                     :commits "[]:tabs spc:checkout"
                     :branches "[]:tabs n:new d:del"
                     :stash "spc:apply g:pop d:drop"
                     "")]
    (into [:row {:gap 1}]
          (remove nil?
                  [[:text {:fg :gray} "q:quit"]
                   [:text {:fg :gray} "?:help"]
                   [:text {:fg :gray} "h/l:panels"]
                   [:text {:fg :gray} "j/k:nav"]
                   (when (seq panel-help)
                     [:text {:fg :gray} panel-help])
                   [:text {:fg :gray} "p/P:pull/push"]]))))

(defn stash-menu-view [{:keys [menu-mode]}]
  (when (= menu-mode :stash-options)
    [:box {:border :double :title "Stash Options" :padding [0 1]}
     [:col
      [:text {:fg :cyan} "a - Stash all changes"]
      [:text {:fg :cyan} "i - Stash all changes and keep index"]
      [:text {:fg :cyan} "U - Stash all including untracked files"]
      [:text {:fg :yellow} "s - Stash staged changes only"]
      [:text {:fg :yellow} "u - Stash unstaged changes only"]
      [:text ""]
      [:text {:fg :gray} "esc - Cancel"]]]))

(defn reset-menu-view [{:keys [menu-mode]}]
  (when (= menu-mode :reset-options)
    [:box {:border :double :title "Reset Options" :padding [0 1]}
     [:col
      [:text {:fg :yellow} "s - Soft reset (uncommit, keep staged)"]
      [:text {:fg :yellow} "m - Mixed reset (uncommit, unstage)"]
      [:text {:fg :red} "h - Hard reset (discard all changes)"]
      [:text ""]
      [:text {:fg :cyan} "u - Unstage all staged changes"]
      [:text {:fg :cyan} "d - Discard all unstaged changes"]
      [:text {:fg :red} "c - Clean untracked files"]
      [:text ""]
      [:text {:fg :gray} "esc - Cancel"]]]))

(defn help-view [{:keys [menu-mode]}]
  (when (= menu-mode :help)
    [:box {:border :double :title "Help - Keybindings" :padding [0 1]}
     [:col
      [:text {:fg :cyan :bold true} "Global:"]
      [:text "  q - Quit                 r - Refresh"]
      [:text "  h/l - Prev/Next panel    2-5 - Jump to panel"]
      [:text "  j/k - Move down/up       z/Z - Undo/Redo"]
      [:text "  p - Pull                 P - Push"]
      [:text "  ? - Help                 D - Reset options"]
      [:text ""]
      [:text {:fg :green :bold true} "Files (2):"]
      [:text "  space - Stage/unstage    a - Stage all"]
      [:text "  c - Commit               d - Discard file"]
      [:text "  s - Quick stash          S - Stash options"]
      [:text ""]
      [:text {:fg :yellow :bold true} "Branches (3):"]
      [:text "  [/] - Switch tabs        enter - Checkout"]
      [:text "  n - New branch           d - Delete branch"]
      [:text "  R - Rename               M - Merge"]
      [:text "  f - Fast-forward"]
      [:text ""]
      [:text {:fg :magenta :bold true} "Commits (4):"]
      [:text "  [/] - Switch tabs        space - Checkout"]
      [:text "  g - Reset to             C - Cherry-pick"]
      [:text "  t - Revert               r - Reword (HEAD only)"]
      [:text "  y - Show SHA"]
      [:text ""]
      [:text {:fg :blue :bold true} "Stash (5):"]
      [:text "  space - Apply            g - Pop (apply+drop)"]
      [:text "  d - Drop                 n - Branch from stash"]
      [:text ""]
      [:text {:fg :gray} "Press ?, q, or esc to close"]]]))

(defn input-view [{:keys [input-mode input-buffer input-context]}]
  (when input-mode
    (let [title (case input-mode
                  :commit "Commit Message"
                  :new-branch "New Branch Name"
                  :rename-branch (str "Rename " input-context " to")
                  :stash-branch "Branch Name from Stash"
                  :reword "Reword Commit Message"
                  (name input-mode))]
      [:box {:border :rounded :title title :padding [0 1] :width 50}
       [:input {:value input-buffer}]])))

(defn message-view [{:keys [message]}]
  (when message
    [:text {:fg :yellow :bold true} (str "  " message)]))

(defn main-grid-view
  "Render the main lazygit-style grid layout."
  [model width height]
  (let [has-message? (some? (:message model))
        narrow? (< width 70)
        ;; Calculate available height for panels
        ;; Wide layout: left column has [3 :flex :flex :flex :flex]
        ;; So 4 flex panels share (height - status(3) - help-bar(1) - message?)
        ;; Each panel total height = remaining / 4
        ;; Inner height = panel total - 2 (borders)
        remaining-height (- height 3 1 (if has-message? 1 0))
        panel-total-height (quot remaining-height 4)
        ;; Inner height is total minus borders (2 rows)
        panel-inner-height (if narrow?
                             ;; Narrow: 6 sections share height
                             (max 1 (- (quot remaining-height 6) 2))
                             ;; Wide: 4 panels share remaining height
                             (max 1 (- panel-total-height 2)))
        content (if narrow?
                  ;; NARROW: Single column stacked
                  [:col {:heights [3 :flex :flex :flex 3 :flex 4]}
                   (status-panel model)
                   (files-panel model panel-inner-height)
                   (branches-panel model panel-inner-height)
                   (commits-panel model panel-inner-height)
                   (stash-panel model panel-inner-height)
                   (main-view-panel model)
                   (command-log-panel)]
                  ;; WIDE: Two columns
                  [:row {:gap 1 :widths [30 :flex]}
                   [:col {:heights [3 :flex :flex :flex :flex]}
                    (status-panel model)
                    (files-panel model panel-inner-height)
                    (branches-panel model panel-inner-height)
                    (commits-panel model panel-inner-height)
                    (stash-panel model panel-inner-height)]
                   [:col {:heights [:flex 4]}
                    (main-view-panel model)
                    (command-log-panel)]])]
    (if has-message?
      [:col {:width width :height height :heights [1 :flex 1]}
       (message-view model)
       content
       (help-bar model)]
      [:col {:width width :height height :heights [:flex 1]}
       content
       (help-bar model)])))

(defn view [model {:keys [width height] :or {width 120 height 30}}]
  (let [background (main-grid-view model width height)]
    (cond
      ;; Help menu modal overlay
      (= (:menu-mode model) :help)
      [:modal {}
       background
       (help-view model)]

      ;; Stash options menu modal overlay
      (= (:menu-mode model) :stash-options)
      [:modal {}
       background
       (stash-menu-view model)]

      ;; Reset options menu modal overlay
      (= (:menu-mode model) :reset-options)
      [:modal {}
       background
       (reset-menu-view model)]

      ;; Input mode modal overlay (commit message, etc.)
      (:input-mode model)
      [:modal {}
       background
       (input-view model)]

      ;; Default grid view
      :else
      background)))

;; === Main ===

(defn -main [& _args]
  (if (git/repo-root)
    (do
      (println "Starting lazygitclj...")
      (tui/run {:init (initial-model)
                :update update-model
                :view view})
      (println "Goodbye!"))
    (do
      (println "Error: Not a git repository")
      (System/exit 1))))
