{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeSynonymInstances #-}

module Backend.Python.Validator (run, PythonValidator (..), PythonValidatorEnv (..)) where

import Control.Monad.Reader
import Convex.Validator
import System.Directory (createDirectoryIfMissing, doesFileExist)
import System.Exit (ExitCode (..))
import System.FilePath ((</>))
import System.IO (hGetContents)
import System.Process (CreateProcess (..), StdStream (..), createProcess, proc, waitForProcess)

newtype PythonValidator a = PythonValidator
  { forall a. PythonValidator a -> ReaderT PythonValidatorEnv IO a
runPythonValidator :: ReaderT PythonValidatorEnv IO a
  }
  deriving ((forall a b. (a -> b) -> PythonValidator a -> PythonValidator b)
-> (forall a b. a -> PythonValidator b -> PythonValidator a)
-> Functor PythonValidator
forall a b. a -> PythonValidator b -> PythonValidator a
forall a b. (a -> b) -> PythonValidator a -> PythonValidator b
forall (f :: * -> *).
(forall a b. (a -> b) -> f a -> f b)
-> (forall a b. a -> f b -> f a) -> Functor f
$cfmap :: forall a b. (a -> b) -> PythonValidator a -> PythonValidator b
fmap :: forall a b. (a -> b) -> PythonValidator a -> PythonValidator b
$c<$ :: forall a b. a -> PythonValidator b -> PythonValidator a
<$ :: forall a b. a -> PythonValidator b -> PythonValidator a
Functor, Functor PythonValidator
Functor PythonValidator =>
(forall a. a -> PythonValidator a)
-> (forall a b.
    PythonValidator (a -> b) -> PythonValidator a -> PythonValidator b)
-> (forall a b c.
    (a -> b -> c)
    -> PythonValidator a -> PythonValidator b -> PythonValidator c)
-> (forall a b.
    PythonValidator a -> PythonValidator b -> PythonValidator b)
-> (forall a b.
    PythonValidator a -> PythonValidator b -> PythonValidator a)
-> Applicative PythonValidator
forall a. a -> PythonValidator a
forall a b.
PythonValidator a -> PythonValidator b -> PythonValidator a
forall a b.
PythonValidator a -> PythonValidator b -> PythonValidator b
forall a b.
PythonValidator (a -> b) -> PythonValidator a -> PythonValidator b
forall a b c.
(a -> b -> c)
-> PythonValidator a -> PythonValidator b -> PythonValidator c
forall (f :: * -> *).
Functor f =>
(forall a. a -> f a)
-> (forall a b. f (a -> b) -> f a -> f b)
-> (forall a b c. (a -> b -> c) -> f a -> f b -> f c)
-> (forall a b. f a -> f b -> f b)
-> (forall a b. f a -> f b -> f a)
-> Applicative f
$cpure :: forall a. a -> PythonValidator a
pure :: forall a. a -> PythonValidator a
$c<*> :: forall a b.
PythonValidator (a -> b) -> PythonValidator a -> PythonValidator b
<*> :: forall a b.
PythonValidator (a -> b) -> PythonValidator a -> PythonValidator b
$cliftA2 :: forall a b c.
(a -> b -> c)
-> PythonValidator a -> PythonValidator b -> PythonValidator c
liftA2 :: forall a b c.
(a -> b -> c)
-> PythonValidator a -> PythonValidator b -> PythonValidator c
$c*> :: forall a b.
PythonValidator a -> PythonValidator b -> PythonValidator b
*> :: forall a b.
PythonValidator a -> PythonValidator b -> PythonValidator b
$c<* :: forall a b.
PythonValidator a -> PythonValidator b -> PythonValidator a
<* :: forall a b.
PythonValidator a -> PythonValidator b -> PythonValidator a
Applicative, Applicative PythonValidator
Applicative PythonValidator =>
(forall a b.
 PythonValidator a -> (a -> PythonValidator b) -> PythonValidator b)
-> (forall a b.
    PythonValidator a -> PythonValidator b -> PythonValidator b)
-> (forall a. a -> PythonValidator a)
-> Monad PythonValidator
forall a. a -> PythonValidator a
forall a b.
PythonValidator a -> PythonValidator b -> PythonValidator b
forall a b.
PythonValidator a -> (a -> PythonValidator b) -> PythonValidator b
forall (m :: * -> *).
Applicative m =>
(forall a b. m a -> (a -> m b) -> m b)
-> (forall a b. m a -> m b -> m b)
-> (forall a. a -> m a)
-> Monad m
$c>>= :: forall a b.
PythonValidator a -> (a -> PythonValidator b) -> PythonValidator b
>>= :: forall a b.
PythonValidator a -> (a -> PythonValidator b) -> PythonValidator b
$c>> :: forall a b.
PythonValidator a -> PythonValidator b -> PythonValidator b
>> :: forall a b.
PythonValidator a -> PythonValidator b -> PythonValidator b
$creturn :: forall a. a -> PythonValidator a
return :: forall a. a -> PythonValidator a
Monad, Monad PythonValidator
Monad PythonValidator =>
(forall a. IO a -> PythonValidator a) -> MonadIO PythonValidator
forall a. IO a -> PythonValidator a
forall (m :: * -> *).
Monad m =>
(forall a. IO a -> m a) -> MonadIO m
$cliftIO :: forall a. IO a -> PythonValidator a
liftIO :: forall a. IO a -> PythonValidator a
MonadIO, MonadReader PythonValidatorEnv, Monad PythonValidator
Monad PythonValidator =>
(forall a. String -> PythonValidator a)
-> MonadFail PythonValidator
forall a. String -> PythonValidator a
forall (m :: * -> *).
Monad m =>
(forall a. String -> m a) -> MonadFail m
$cfail :: forall a. String -> PythonValidator a
fail :: forall a. String -> PythonValidator a
MonadFail)

data PythonValidatorEnv = PythonValidatorEnv {PythonValidatorEnv -> String
projectPath :: FilePath}

run :: PythonValidatorEnv -> PythonValidator a -> IO a
run :: forall a. PythonValidatorEnv -> PythonValidator a -> IO a
run PythonValidatorEnv
renv PythonValidator a
action = ReaderT PythonValidatorEnv IO a -> PythonValidatorEnv -> IO a
forall r (m :: * -> *) a. ReaderT r m a -> r -> m a
runReaderT (PythonValidator a -> ReaderT PythonValidatorEnv IO a
forall a. PythonValidator a -> ReaderT PythonValidatorEnv IO a
runPythonValidator PythonValidator a
action) PythonValidatorEnv
renv

instance Validator PythonValidator where
  -- \| Sets up the Python validation sandbox project.
  setup :: PythonValidator ()
setup = do
    PythonValidatorEnv
config <- PythonValidator PythonValidatorEnv
forall r (m :: * -> *). MonadReader r m => m r
ask
    let pythonProjectPath :: String
pythonProjectPath = PythonValidatorEnv -> String
projectPath PythonValidatorEnv
config
    let stubsPath :: String
stubsPath = String
pythonProjectPath String -> String -> String
</> String
"stubs"

    -- Create the directory structure.
    IO () -> PythonValidator ()
forall a. IO a -> PythonValidator a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO () -> PythonValidator ()) -> IO () -> PythonValidator ()
forall a b. (a -> b) -> a -> b
$ Bool -> String -> IO ()
createDirectoryIfMissing Bool
True String
stubsPath

    -- Write the mypy.ini config file.
    IO () -> PythonValidator ()
forall a. IO a -> PythonValidator a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO () -> PythonValidator ()) -> IO () -> PythonValidator ()
forall a b. (a -> b) -> a -> b
$
      String -> IO Bool
doesFileExist (String
pythonProjectPath String -> String -> String
</> String
"mypy.ini") IO Bool -> (Bool -> IO ()) -> IO ()
forall a b. IO a -> (a -> IO b) -> IO b
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= \case
        Bool
True -> () -> IO ()
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return () -- If it exists, we don't overwrite it.
        Bool
False -> String -> String -> IO ()
writeFile (String
pythonProjectPath String -> String -> String
</> String
"mypy.ini") String
mypyIniContent

    -- Write the pydantic stub file.
    IO () -> PythonValidator ()
forall a. IO a -> PythonValidator a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO () -> PythonValidator ()) -> IO () -> PythonValidator ()
forall a b. (a -> b) -> a -> b
$
      String -> IO Bool
doesFileExist (String
stubsPath String -> String -> String
</> String
"pydantic.pyi") IO Bool -> (Bool -> IO ()) -> IO ()
forall a b. IO a -> (a -> IO b) -> IO b
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= \case
        Bool
True -> () -> IO ()
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return () -- If it exists, we don't overwrite it.
        Bool
False -> String -> String -> IO ()
writeFile (String
stubsPath String -> String -> String
</> String
"pydantic.pyi") String
pydanticStubContent

    -- Write the convex stub file.
    IO () -> PythonValidator ()
forall a. IO a -> PythonValidator a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO () -> PythonValidator ()) -> IO () -> PythonValidator ()
forall a b. (a -> b) -> a -> b
$
      String -> IO Bool
doesFileExist (String
stubsPath String -> String -> String
</> String
"convex.pyi") IO Bool -> (Bool -> IO ()) -> IO ()
forall a b. IO a -> (a -> IO b) -> IO b
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= \case
        Bool
True -> () -> IO ()
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return () -- If it exists, we don't overwrite it.
        Bool
False -> String -> String -> IO ()
writeFile (String
stubsPath String -> String -> String
</> String
"convex.pyi") String
convexStubContent

    -- Write an empty stub for pydantic_core to satisfy mypy
    IO () -> PythonValidator ()
forall a. IO a -> PythonValidator a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO () -> PythonValidator ()) -> IO () -> PythonValidator ()
forall a b. (a -> b) -> a -> b
$
      String -> IO Bool
doesFileExist (String
stubsPath String -> String -> String
</> String
"pydantic_core.pyi") IO Bool -> (Bool -> IO ()) -> IO ()
forall a b. IO a -> (a -> IO b) -> IO b
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= \case
        Bool
True -> () -> IO ()
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return () -- If it exists, we don't overwrite it.
        Bool
False -> String -> String -> IO ()
writeFile (String
stubsPath String -> String -> String
</> String
"pydantic_core.pyi") String
""

    () -> PythonValidator ()
forall a. a -> PythonValidator a
forall (m :: * -> *) a. Monad m => a -> m a
return ()

  -- \| Validates the generated Python code using mypy.
  validate :: String -> PythonValidator (Maybe String)
validate String
generatedCode = do
    String
pythonProjectPath <- (PythonValidatorEnv -> String) -> PythonValidator String
forall r (m :: * -> *) a. MonadReader r m => (r -> a) -> m a
asks PythonValidatorEnv -> String
projectPath
    let generatedFilePath :: String
generatedFilePath = String
pythonProjectPath String -> String -> String
</> String
"generated_api.py"

    -- Write the generated code to the sandbox project.
    IO () -> PythonValidator ()
forall a. IO a -> PythonValidator a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO () -> PythonValidator ()) -> IO () -> PythonValidator ()
forall a b. (a -> b) -> a -> b
$ String -> String -> IO ()
writeFile String
generatedFilePath String
generatedCode

    -- Run `mypy` on the generated file.
    IO () -> PythonValidator ()
forall a. IO a -> PythonValidator a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO () -> PythonValidator ()) -> IO () -> PythonValidator ()
forall a b. (a -> b) -> a -> b
$ String -> IO ()
putStrLn String
"[Validator] Running 'mypy'..."
    -- We run `mypy .` to make it pick up the mypy.ini config automatically.
    let mypyCmd :: CreateProcess
mypyCmd = (String -> [String] -> CreateProcess
proc String
"mypy" [String
"."]) {cwd = Just pythonProjectPath, std_out = CreatePipe, std_err = CreatePipe}
    (Maybe Handle
_, Just Handle
hOut, Just Handle
hErr, ProcessHandle
handle) <- IO (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
-> PythonValidator
     (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
forall a. IO a -> PythonValidator a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
 -> PythonValidator
      (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle))
-> IO (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
-> PythonValidator
     (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
forall a b. (a -> b) -> a -> b
$ CreateProcess
-> IO (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
createProcess CreateProcess
mypyCmd
    ExitCode
exitCode <- IO ExitCode -> PythonValidator ExitCode
forall a. IO a -> PythonValidator a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO ExitCode -> PythonValidator ExitCode)
-> IO ExitCode -> PythonValidator ExitCode
forall a b. (a -> b) -> a -> b
$ ProcessHandle -> IO ExitCode
waitForProcess ProcessHandle
handle

    if ExitCode
exitCode ExitCode -> ExitCode -> Bool
forall a. Eq a => a -> a -> Bool
== ExitCode
ExitSuccess
      then do
        IO () -> PythonValidator ()
forall a. IO a -> PythonValidator a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO () -> PythonValidator ()) -> IO () -> PythonValidator ()
forall a b. (a -> b) -> a -> b
$ String -> IO ()
putStrLn String
"[Validator] Validation successful."
        String
content <- IO String -> PythonValidator String
forall a. IO a -> PythonValidator a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO String -> PythonValidator String)
-> IO String -> PythonValidator String
forall a b. (a -> b) -> a -> b
$ String -> IO String
readFile String
generatedFilePath
        Maybe String -> PythonValidator (Maybe String)
forall a. a -> PythonValidator a
forall (m :: * -> *) a. Monad m => a -> m a
return (Maybe String -> PythonValidator (Maybe String))
-> Maybe String -> PythonValidator (Maybe String)
forall a b. (a -> b) -> a -> b
$ String -> Maybe String
forall a. a -> Maybe a
Just String
content
      else do
        IO () -> PythonValidator ()
forall a. IO a -> PythonValidator a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO () -> PythonValidator ()) -> IO () -> PythonValidator ()
forall a b. (a -> b) -> a -> b
$ String -> IO ()
putStrLn String
"[Validator] Error: 'mypy' failed. The generated code has type errors."
        -- Print both stdout and stderr for comprehensive error reporting.
        IO () -> PythonValidator ()
forall a. IO a -> PythonValidator a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO () -> PythonValidator ()) -> IO () -> PythonValidator ()
forall a b. (a -> b) -> a -> b
$ Handle -> IO String
hGetContents Handle
hOut IO String -> (String -> IO ()) -> IO ()
forall a b. IO a -> (a -> IO b) -> IO b
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= String -> IO ()
putStrLn
        IO () -> PythonValidator ()
forall a. IO a -> PythonValidator a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO () -> PythonValidator ()) -> IO () -> PythonValidator ()
forall a b. (a -> b) -> a -> b
$ Handle -> IO String
hGetContents Handle
hErr IO String -> (String -> IO ()) -> IO ()
forall a b. IO a -> (a -> IO b) -> IO b
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= String -> IO ()
putStrLn
        Maybe String -> PythonValidator (Maybe String)
forall a. a -> PythonValidator a
forall (m :: * -> *) a. Monad m => a -> m a
return Maybe String
forall a. Maybe a
Nothing

-- | Content for the mypy.ini configuration file.
mypyIniContent :: String
mypyIniContent :: String
mypyIniContent =
  [String] -> String
unlines
    [ String
"[mypy]",
      String
"python_version = 3.9",
      String
"disallow_untyped_defs = True",
      String
"check_untyped_defs = True",
      String
"warn_return_any = True",
      String
"ignore_missing_imports = True"
    ]

-- | Minimal stub content for the `pydantic` library.
pydanticStubContent :: String
pydanticStubContent :: String
pydanticStubContent =
  [String] -> String
unlines
    [ String
"from typing import Any, Type",
      String
"",
      String
"class BaseModel:",
      String
"    def model_validate(cls: Type, obj: Any) -> Any: ...",
      String
"    def model_dump_json(self, *, indent: int | None = ...) -> str: ...",
      String
"",
      String
"def Field(default: Any = ..., *, alias: str | None = ...) -> Any: ...",
      String
"",
      String
"class TypeAdapter:",
      String
"    def __init__(self, type: Any) -> None: ...",
      String
"    def validate_python(self, data: Any) -> Any: ...",
      String
"",
      String
"class ValidationError(Exception): ..."
    ]

-- | Minimal stub content for the `convex` library.
convexStubContent :: String
convexStubContent :: String
convexStubContent =
  [String] -> String
unlines
    [ String
"from typing import Any, Iterator",
      String
"",
      String
"class ConvexClient:",
      String
"    def __init__(self, url: str) -> None: ...",
      String
"    def set_admin_auth(self, key: str) -> None: ...",
      String
"    def set_auth(self, token: str) -> None: ...",
      String
"    def query(self, path: str, args: dict[str, Any]) -> Any: ...",
      String
"    def mutation(self, path: str, args: dict[str, Any]) -> Any: ...",
      String
"    def action(self, path: str, args: dict[str, Any]) -> Any: ...",
      String
"    def subscribe(self, path: str, args: dict[str, Any]) -> Iterator[Any]: ...",
      String
"",
      String
"class ConvexError(Exception): ..."
    ]