!> Watch loop engine and watcher state.
!!
!! This module contains the `watcher_t` type, which:
!! - Builds and maintains the watch list,
!! - Tracks file fingerprints for change detection,
!! - Runs `fpm` commands when changes are detected,
!! - Coordinates optional feature plugins.
!!
!! The engine is intentionally "poll-based" to remain portable across platforms.
module watch_engine
   use, intrinsic :: iso_fortran_env, only: int64
   use watch_types, only: watch_config_t, root_info_t
   use watch_feature_manager, only: feature_manager_t
   use watch_feature_factory, only: enable_features
   use watch_log, only: log_info
   use watch_fpm_graph, only: compute_watch_files_from_settings
   use watch_fingerprint, only: init_fingerprints, scan_changes, accept_changes, manifest_key_from_files
   use watch_time, only: sleep_seconds, set_low_cpu
   use watch_exec, only: report_changes, build_run_command, run_command_and_report
   use watch_banner, only: print_banner, print_file_list
   use watch_util, only: ftoa
   use fpm_strings, only: string_t, str
   use fpm_filesystem, only: basename, exists
   implicit none
   private
   public watcher_t

   !> Main watcher state and methods.
   !!
   !! `watcher_t` owns:
   !! - The effective configuration (`cfg`)
   !! - Feature plugins (`fm`)
   !! - Watch list (`files`) plus per-file target masks (`file_mask`)
   !! - Fingerprinting buffers for change detection (`fp_prev`, `fp_now`)
   !! - The current manifest key used to detect "rebuild needed" events (`man_prev`)
   type watcher_t
      type(watch_config_t) :: cfg
      type(feature_manager_t) :: fm

      type(string_t), allocatable :: files(:)
      integer(int64), allocatable :: file_mask(:)
      type(root_info_t), allocatable :: roots(:)

      integer(int64), allocatable :: fp_prev(:)
      integer(int64), allocatable :: fp_now(:)
      integer, allocatable :: changed_idx(:)

      integer(int64) :: man_prev = 0_int64
   contains
      procedure :: init => watcher_init
      procedure :: run  => watcher_run
   end type watcher_t

contains

   !> Initialize a watcher instance from configuration.
   !!
   !! This routine:
   !! - Applies low-CPU mode selection to the sleep implementation.
   !! - Enables requested feature plugins and calls their init/start callbacks.
   !! - Builds the initial watch list and fingerprints.
   !! - Optionally performs an initial run (`w%run_on_start`).
   subroutine watcher_init(self, cfg)
      class(watcher_t), intent(inout) :: self
      type(watch_config_t), intent(in) :: cfg

      self%cfg = cfg
      call set_low_cpu(self%cfg%w%low_cpu)

      call enable_features(self%fm, self%cfg%w)
      call self%fm%init_all(self%cfg)
      call self%fm%on_start_all()

      call rebuild_watch_list(self, print_header=.true.)

      if (self%cfg%w%run_on_start) then
         call run_once(self, trim(self%cfg%fpm_cmdline))
      end if
   end subroutine watcher_init

   !> Run the main watch loop until termination.
   !!
   !! Termination conditions:
   !! - The sentinel file `.fpm-watch.stop` exists, or
   !! - The process receives an external termination (e.g., Ctrl+C).
   !!
   !! Each iteration performs:
   !! 1. Sleep for `poll` seconds.
   !! 2. Scan for changes (fingerprints).
   !! 3. If changes were found, sleep for `debounce` seconds and rescan.
   !! 4. If `fpm.toml` changed, rebuild watch list.
   !! 5. Otherwise, run the computed `fpm` command (with target injection
   !!    for run/test), notify plugins, and accept fingerprints.
   subroutine watcher_run(self)
      class(watcher_t), intent(inout) :: self

      integer :: changed_count
      type(string_t), allocatable :: changed(:)
      character(len=:), allocatable :: cmd
      integer :: exitstat, j, idx
      real :: secs

      integer(int64) :: rate, t_last_rescan, t_now
      real :: dt
      if (self%cfg%w%once) return

      call system_clock(count_rate=rate)
      if (rate <= 0_int64) rate = 1000_int64
      call system_clock(t_last_rescan)

      do
         if (exists(".fpm-watch.stop")) exit

         call sleep_seconds(self%cfg%w%poll)

         call scan_changes(self%files, self%fp_prev, self%fp_now, self%changed_idx, changed_count)

         if (changed_count > 0) then
            call sleep_seconds(self%cfg%w%debounce)
            call scan_changes(self%files, self%fp_prev, self%fp_now, self%changed_idx, changed_count)
         end if

         if (changed_count > 0) then
            if (any_manifest_changed(self%files, self%changed_idx, changed_count)) then
               call handle_manifest_change(self)
               cycle
            end if

            allocate(changed(changed_count))
            do j = 1, changed_count
               idx = self%changed_idx(j)
               if (idx >= 1 .and. idx <= size(self%files)) then
                  changed(j)%s = self%files(idx)%s
               else
                  changed(j)%s = ""
               end if
            end do

            call self%fm%on_change_detected_all(changed)
            call report_changes(self%files, self%changed_idx, changed_count, self%cfg%w)

            cmd = build_run_command( &
               settings      = self%cfg%settings,    &
               full_cmdline  = self%cfg%fpm_cmdline, &
               cmd_prefix    = self%cfg%cmd_prefix,  &
               cmd_rest      = self%cfg%cmd_rest,    &
               roots         = self%roots,           &
               file_mask     = self%file_mask,       &
               changed_idx   = self%changed_idx,     &
               changed_count = changed_count )

            call self%fm%on_before_run_all(cmd)
            call run_command_and_report(cmd, self%cfg%w, exitstat, secs)
            call self%fm%on_after_run_all(exitstat, secs)

            call accept_changes(self%fp_prev, self%fp_now, self%changed_idx, changed_count)

            deallocate(changed)
         end if

         if (self%cfg%w%rescan > 0.0) then
            call system_clock(t_now)
            if (t_now < t_last_rescan) then
               t_last_rescan = t_now
            else
               dt = real(t_now - t_last_rescan) / real(rate)
               if (dt >= self%cfg%w%rescan) then
                  call log_info(self%cfg%w, "rescan triggered -> rebuilding watch list")
                  call rebuild_watch_list(self, print_header=.false.)
                  call system_clock(t_last_rescan)
               end if
            end if
         end if
      end do
   end subroutine watcher_run

   !> Handle a manifest change (`fpm.toml`) by rebuilding the watch list.
   !!
   !! This also notifies feature plugins via `on_manifest_changed`.
   subroutine handle_manifest_change(self)
      class(watcher_t), intent(inout) :: self
      integer(int64) :: man_now

      man_now = manifest_key_from_files(self%cfg%settings, self%files)
      if (man_now /= self%man_prev) then
         call self%fm%on_manifest_changed_all(self%man_prev, man_now)
         self%man_prev = man_now
      end if
      call log_info(self%cfg%w, "manifest changed -> rebuilding watch list")
      call rebuild_watch_list(self, print_header=.false.)
   end subroutine handle_manifest_change

   !> Return whether the changed set contains `fpm.toml`.
   !!
   !! This detection is based on the basename only and therefore triggers if
   !! any watched file named `fpm.toml` changes (main or dependency manifests).
   logical function any_manifest_changed(files, changed_idx, changed_count) result(hit)
      type(string_t), allocatable, intent(in) :: files(:)
      integer, intent(in) :: changed_idx(:)
      integer, intent(in) :: changed_count
      integer :: j, idx
      character(len=:), allocatable :: b
      hit = .false.
      if (.not. allocated(files)) return
      do j = 1, changed_count
         idx = changed_idx(j)
         if (idx < 1 .or. idx > size(files)) cycle
         b = basename(files(idx)%s)
         if (b == "fpm.toml") then
            hit = .true.
            return
         end if
      end do
   end function any_manifest_changed

   !> Run the configured command once without change detection.
   !!
   !! Used for the optional "run on start" behavior.
   subroutine run_once(self, cmd0)
      class(watcher_t), intent(inout) :: self
      character(len=*), intent(in) :: cmd0
      character(len=:), allocatable :: cmd
      integer :: exitstat
      real :: secs
      cmd = trim(cmd0)
      call self%fm%on_before_run_all(cmd)
      call run_command_and_report(cmd, self%cfg%w, exitstat, secs)
      call self%fm%on_after_run_all(exitstat, secs)
   end subroutine run_once

   !> Recompute the watch list, rebuild fingerprints, and print/log the result.
   !!
   !! Called:
   !! - During initialization,
   !! - When the manifest changes,
   !! - Periodically when `rescan > 0`.
   subroutine rebuild_watch_list(self, print_header)
      class(watcher_t), intent(inout) :: self
      logical, intent(in) :: print_header

      integer(int64) :: rate, t0, t1
      real :: build_secs_total
      real :: secs_model, secs_targets, secs_watch
      integer :: nfiles, nroots
      real :: dummy_build

      type(string_t), allocatable :: new_files(:)
      integer(int64), allocatable :: new_mask(:)
      type(root_info_t), allocatable :: new_roots(:)

      call system_clock(count_rate=rate)
      if (rate <= 0_int64) rate = 1000_int64
      call system_clock(t0)

      call compute_watch_files_from_settings( &
         settings     = self%cfg%settings, &
         w            = self%cfg%w,        &
         files        = new_files,         &
         file_mask    = new_mask,          &
         roots        = new_roots,         &
         manifest_key = self%man_prev,     &
         build_secs   = dummy_build,       &
         secs_model   = secs_model,        &
         secs_targets = secs_targets,      &
         secs_watch   = secs_watch )

      call move_alloc(new_files, self%files)
      call move_alloc(new_mask,  self%file_mask)
      call move_alloc(new_roots, self%roots)

      call self%fm%on_watch_list_built_all(self%files, self%roots)

      self%man_prev = manifest_key_from_files(self%cfg%settings, self%files)

      call init_fingerprints(self%files, self%fp_prev, self%fp_now, self%changed_idx)

      call system_clock(t1)
      build_secs_total = real(t1 - t0) / real(rate)

      nfiles = 0
      if (allocated(self%files)) nfiles = size(self%files)

      nroots = 0
      if (allocated(self%roots)) nroots = size(self%roots)

      if (self%cfg%w%debug) then
         call log_info(self%cfg%w, "debug: manifest_key=" // str(self%man_prev))
         call log_info(self%cfg%w, "debug: roots=" // str(nroots) // " files=" // str(nfiles))
      end if

      if (print_header) then
         call print_banner( &
            full_cmdline = self%cfg%fpm_cmdline, &
            settings     = self%cfg%settings,    &
            w            = self%cfg%w,           &
            files        = self%files,           &
            build_secs   = build_secs_total,     &
            roots_count  = nroots,               &
            secs_model   = secs_model,           &
            secs_targets = secs_targets,         &
            secs_watch   = secs_watch )
         if (self%cfg%w%print_files_once) call print_file_list(self%files, self%cfg%w)
      else
         call log_info(self%cfg%w, "watch list updated in " // ftoa(build_secs_total) // "s; files=" // str(nfiles))
         if (self%cfg%w%verbosity >= 2) call print_file_list(self%files, self%cfg%w)
      end if
   end subroutine rebuild_watch_list

end module watch_engine
