content: {-# LANGUAGE BangPatterns #-}
  {-# LANGUAGE LambdaCase #-}
  {-# LANGUAGE RecordWildCards #-}
  {-# LANGUAGE ScopedTypeVariables #-}
  
  module Dev (runDevMode, DevOptions (..)) where
  
  import qualified Backend.Python as Python
  import qualified Backend.Python.Validator as Python.Validator
  import qualified Backend.Rust as Rust
  import qualified Backend.Rust.Validator as Rust.Validator
  import qualified Config
  import Control.Concurrent (forkIO, threadDelay)
  import Control.Concurrent.STM (TVar, atomically, newTVarIO, readTVar, retry, writeTVar)
  import Control.DeepSeq (rnf)
  import Control.Exception
  import Control.Exception (evaluate)
  import Control.Monad (forM_, forever, when)
  import Control.Monad.IO.Class (MonadIO, liftIO)
  import qualified Convex.Parser as P
  import qualified Convex.Validator as Validator
  import System.Directory (doesFileExist, findExecutable)
  import System.Exit (ExitCode (..), exitFailure)
  import System.FSNotify
  import System.IO.Error (isAlreadyInUseError)
  import System.Process (CreateProcess (..), StdStream (..), createProcess, proc, waitForProcess)
  
  data DevOptions = DevOptions
    { convexProjectPath :: FilePath,
      declarationsDir :: FilePath,
      config :: Config.Config
    }
  
  -- | Searches for an available package manager, preferring pnpm.
  findPackageManager :: IO (Maybe String)
  findPackageManager = do
    pnpmPath <- findExecutable "pnpm"
    case pnpmPath of
      Just _ -> return (Just "pnpm")
      Nothing -> do
        npmPath <- findExecutable "npm"
        case npmPath of
          Just _ -> return (Just "npm")
          Nothing -> return Nothing
  
  -- | Runs the development mode file watcher with debouncing.
  runDevMode :: Config.Config -> IO ()
  runDevMode config = do
    -- TVar to signal that a change has occurred.
    dirtyVar <- newTVarIO True -- Start dirty to trigger an initial run.
  
    -- Fork a worker thread that handles the generation.
    _ <- forkIO (workerThread dirtyVar config)
  
    -- The main thread will watch for file changes.
    withManager $ \mgr -> do
      putStrLn $ "Watching for changes in: " ++ convexPath
      watchTree
        mgr
        convexPath
        (const True) -- Watch all events
        ( \event -> do
            putStrLn $ "[Watcher] Detected change: " ++ showEvent event
            -- Atomically set the dirty flag. This is fast and non-blocking.
            atomically $ writeTVar dirtyVar True
        )
  
      -- Keep the main thread alive.
      forever $ threadDelay 1000000
    where
      convexPath = Config.projectPath config ++ "/convex"
      showEvent (Added path _ _) = "Added: " ++ path
      showEvent (Modified path _ _) = "Modified: " ++ path
      showEvent (Removed path _ _) = "Removed: " ++ path
      showEvent _ = "Unknown event"
  
  -- | The worker thread loop that performs debouncing.
  workerThread :: TVar Bool -> Config.Config -> IO ()
  workerThread dirtyVar config = forever $ do
    -- Wait for a signal that something has changed.
    atomically $ do
      dirty <- readTVar dirtyVar
      -- The `check` function will retry the transaction if the condition is false.
      -- This effectively makes the thread sleep until `dirty` is True.
      when (not dirty) retry
  
    -- Once signaled, reset the flag and wait for the debounce period.
    atomically $ writeTVar dirtyVar False
    putStrLn "[Worker] Change detected. Debouncing for 300ms..."
    threadDelay 300000 -- 300ms debounce period.
  
    -- After debouncing, check if another change came in. If not, generate.
    isDirty <- atomically (readTVar dirtyVar)
    if isDirty
      then putStrLn "[Worker] Further changes detected. Restarting debounce timer."
      else
        handleGenerationEvent config `catch` \(e :: SomeException) -> do
          putStrLn $ "[Worker] Error during generation: " ++ show e
  
  -- | Handles the logic for regenerating declarations and the clients.
  handleGenerationEvent :: Config.Config -> IO ()
  handleGenerationEvent config = do
    findPackageManager >>= \case
      Nothing -> putStrLn "\n[ERROR] Neither 'pnpm' nor 'npm' could be found in your system's PATH.\nPlease install one of them to use the dev watcher.\n"
      Just pm -> do
        putStrLn "[Worker] Regenerating .d.ts files..."
        let pkgMgrCmd = (proc pm ["declarations"]) {cwd = Just $ Config.projectPath config}
        (_, _, _, pkgMgrHandle) <- createProcess pkgMgrCmd
        exitCode <- waitForProcess pkgMgrHandle
        case exitCode of
          ExitSuccess -> do
            putStrLn "[Worker] Declaration files regenerated successfully."
            generateAndWriteCode config
          ExitFailure code ->
            putStrLn $ "[Worker] Error: 'pnpm declarations' failed with exit code " ++ show code
  
  -- | Generates the code and writes it to the target files if it has changed.
  generateAndWriteCode :: Config.Config -> IO ()
  generateAndWriteCode config = do
    putStrLn "[Worker] Parsing schema and generating clients..."
    projectResult <- P.parseProject (Config.projectPath config ++ "/convex/schema.ts") $ Config.declarationsDir config
    case projectResult of
      Left err ->
        putStrLn $ "[Worker] Error parsing project: " ++ err
      Right project ->
        -- Iterate over each target defined in the config
        forM_ (Config.targetList config) $ \Config.TargetConfig {..} -> do
          putStrLn $ "[Worker] Generating for target: " ++ show lang
          let generatedCode = case lang of
                Config.Python -> Python.generatePythonCode project
                Config.Rust -> Rust.generateRustCode project
  
          let validatorAction :: (MonadFail m, MonadIO m, Validator.Validator m) => m String
              validatorAction = do
                Validator.setup
                Validator.validate generatedCode >>= \case
                  Just formattedAndChecked -> do
                    liftIO $ putStrLn "[Worker] Rust code validation successful."
                    return formattedAndChecked
                  Nothing -> do
                    fail "[Worker] Rust code validation failed. Check the output above."
  
          checkedCode <- case lang of
            Config.Rust -> do
              Rust.Validator.run (Rust.Validator.RustValidatorEnv (Config.validationPath config ++ "/rust_validation_project")) validatorAction
            Config.Python -> do
              -- Python.Validator.run (Python.Validator.PythonValidatorEnv (Config.projectPath config ++ "/python_validation_project")) validatorAction
              return generatedCode
  
          -- Write to each output file defined for the target
          forM_ output $ \path -> do
            !oldContentResult <- readTargetFileWithRetry path
            putStrLn $ "[Worker] Checking " ++ path ++ " for changes..."
            case oldContentResult of
              Left err -> putStrLn $ "[Worker] Error reading existing file: " ++ show err
              Right oldContent ->
                if checkedCode /= oldContent
                  then do
                    putStrLn $ "[Worker]   -> Changes detected for " ++ path
                    putStrLn "[Worker]   -> Writing new content..."
                    writeResult <- writeFileWithRetry path checkedCode
                    case writeResult of
                      Left err -> putStrLn $ "[Worker] Error writing file: " ++ show err
                      Right () -> putStrLn $ "[Worker]   -> Success! Updated " ++ path
                  else
                    putStrLn $ "[Worker]   -> No changes detected for " ++ path
  
  -- | Safely reads the content of a file, retrying on busy errors.
  readTargetFileWithRetry :: FilePath -> IO (Either IOException String)
  readTargetFileWithRetry path = do
    exists <- doesFileExist path
    if exists
      then tryWithRetries 10 200000 $ do
        -- 10 retries, 200ms apart
        content <- readFile path
        _ <- evaluate (rnf content) -- Force evaluation to ensure the file handle is released.
        return content
      else return (Right "")
  
  -- | Writes content to a file, retrying on busy errors.
  writeFileWithRetry :: FilePath -> String -> IO (Either IOException ())
  writeFileWithRetry path content = tryWithRetries 10 200000 (writeFile path content)
  
  -- | A helper to retry an IO action if it fails due to a "resource busy" error.
  tryWithRetries :: Int -> Int -> IO a -> IO (Either IOException a)
  tryWithRetries 0 _ action = try action -- Last attempt, return the result
  tryWithRetries n delayMicroseconds action = do
    result <- try action
    case result of
      Left e | isAlreadyInUseError e -> do
        putStrLn $ "[Retry] File is busy, retrying in " ++ show (delayMicroseconds `div` 1000) ++ "ms..."
        threadDelay delayMicroseconds
        tryWithRetries (n - 1) delayMicroseconds action
      _ -> return result
  