{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}

-- |
-- Module      : Test.Hspec.BenchGolden.CSV
-- Description : CSV export for parameter sweep benchmark results
-- Copyright   : (c) 2026
-- License     : MIT
-- Maintainer  : @ocramz
--
-- This module provides CSV export functionality for parameter sweep benchmarks.
-- CSV files are written alongside golden files for convenient analysis and plotting.
--
-- = CSV Format
--
-- The CSV includes columns for:
--
-- * @timestamp@ - When the benchmark was run (ISO 8601 format)
-- * @param_name@ - The name of the sweep parameter
-- * @param_value@ - The value of the parameter for this row
-- * @mean_ms@ - Mean execution time in milliseconds
-- * @stddev_ms@ - Standard deviation in milliseconds
-- * @median_ms@ - Median execution time in milliseconds
-- * @min_ms@ - Minimum execution time in milliseconds
-- * @max_ms@ - Maximum execution time in milliseconds
-- * @trimmed_mean_ms@ - Trimmed mean (if robust statistics enabled)
-- * @mad_ms@ - Median absolute deviation (if robust statistics enabled)
-- * @iqr_ms@ - Interquartile range (if robust statistics enabled)
--
-- = Usage
--
-- CSV files are automatically generated when using 'benchGoldenSweep' or
-- 'benchGoldenSweepWith'. The file is written to:
--
-- @
-- \<outputDir\>/\<sweep-name\>-\<arch-id\>.csv
-- @
--
-- For example: @.golden/sort-scaling-aarch64-darwin-Apple_M1.csv@

module Test.Hspec.BenchGolden.CSV
  ( -- * CSV Generation
    csvHeader
  , csvRow
  , buildCSV

    -- * File I/O
  , writeSweepCSV
  , getSweepCSVPath
  ) where

import Data.Text (Text)
import qualified Data.Text as T
import Data.Text.Lazy.Builder (Builder)
import qualified Data.Text.Lazy.Builder as B
import qualified Data.Text.Lazy.Builder.RealFloat as B
import qualified Data.Text.Lazy.IO as TL
import Data.Time.Format.ISO8601 (iso8601Show)
import System.Directory (createDirectoryIfMissing)
import System.FilePath ((</>), (<.>))

import Test.Hspec.BenchGolden.Arch (sanitizeForFilename)
import Test.Hspec.BenchGolden.Types (GoldenStats(..))

-- | Generate CSV header row.
--
-- The header includes all standard statistics columns plus a parameter column.
csvHeader :: Text -> Builder
csvHeader :: Text -> Builder
csvHeader Text
paramName = [Builder] -> Builder
forall a. Monoid a => [a] -> a
mconcat
  [ Builder
"timestamp,"
  , Text -> Builder
B.fromText Text
paramName, Builder
","
  , Builder
"mean_ms,stddev_ms,median_ms,min_ms,max_ms,"
  , Builder
"trimmed_mean_ms,mad_ms,iqr_ms\n"
  ]

-- | Generate a single CSV data row from benchmark stats.
--
-- The timestamp is taken from the 'GoldenStats', and the parameter value
-- is provided separately as a string representation.
csvRow :: Text -> GoldenStats -> Builder
csvRow :: Text -> GoldenStats -> Builder
csvRow Text
paramValue GoldenStats{Double
[Double]
[(Int, Double)]
Text
UTCTime
statsMean :: Double
statsStddev :: Double
statsMedian :: Double
statsMin :: Double
statsMax :: Double
statsPercentiles :: [(Int, Double)]
statsArch :: Text
statsTimestamp :: UTCTime
statsTrimmedMean :: Double
statsMAD :: Double
statsIQR :: Double
statsOutliers :: [Double]
statsOutliers :: GoldenStats -> [Double]
statsIQR :: GoldenStats -> Double
statsMAD :: GoldenStats -> Double
statsTrimmedMean :: GoldenStats -> Double
statsTimestamp :: GoldenStats -> UTCTime
statsArch :: GoldenStats -> Text
statsPercentiles :: GoldenStats -> [(Int, Double)]
statsMax :: GoldenStats -> Double
statsMin :: GoldenStats -> Double
statsMedian :: GoldenStats -> Double
statsStddev :: GoldenStats -> Double
statsMean :: GoldenStats -> Double
..} = [Builder] -> Builder
forall a. Monoid a => [a] -> a
mconcat
  [ FilePath -> Builder
B.fromString (UTCTime -> FilePath
forall t. ISO8601 t => t -> FilePath
iso8601Show UTCTime
statsTimestamp), Builder
","
  , Text -> Builder
B.fromText Text
paramValue, Builder
","
  , Double -> Builder
forall a. RealFloat a => a -> Builder
B.realFloat Double
statsMean, Builder
","
  , Double -> Builder
forall a. RealFloat a => a -> Builder
B.realFloat Double
statsStddev, Builder
","
  , Double -> Builder
forall a. RealFloat a => a -> Builder
B.realFloat Double
statsMedian, Builder
","
  , Double -> Builder
forall a. RealFloat a => a -> Builder
B.realFloat Double
statsMin, Builder
","
  , Double -> Builder
forall a. RealFloat a => a -> Builder
B.realFloat Double
statsMax, Builder
","
  , Double -> Builder
forall a. RealFloat a => a -> Builder
B.realFloat Double
statsTrimmedMean, Builder
","
  , Double -> Builder
forall a. RealFloat a => a -> Builder
B.realFloat Double
statsMAD, Builder
","
  , Double -> Builder
forall a. RealFloat a => a -> Builder
B.realFloat Double
statsIQR, Builder
"\n"
  ]

-- | Build complete CSV content from a list of (param value, stats) pairs.
--
-- Example:
--
-- @
-- let results = [("1000", stats1), ("5000", stats2), ("10000", stats3)]
-- let csv = buildCSV "n" results
-- @
buildCSV :: Text -> [(Text, GoldenStats)] -> Builder
buildCSV :: Text -> [(Text, GoldenStats)] -> Builder
buildCSV Text
paramName [(Text, GoldenStats)]
rows =
  Text -> Builder
csvHeader Text
paramName Builder -> Builder -> Builder
forall a. Semigroup a => a -> a -> a
<> [Builder] -> Builder
forall a. Monoid a => [a] -> a
mconcat (((Text, GoldenStats) -> Builder)
-> [(Text, GoldenStats)] -> [Builder]
forall a b. (a -> b) -> [a] -> [b]
map ((Text -> GoldenStats -> Builder) -> (Text, GoldenStats) -> Builder
forall a b c. (a -> b -> c) -> (a, b) -> c
uncurry Text -> GoldenStats -> Builder
csvRow) [(Text, GoldenStats)]
rows)

-- | Get the path for a sweep CSV file.
--
-- The file is placed in the output directory with the architecture ID
-- included in the filename (not as a subdirectory) for easy comparison
-- across architectures.
--
-- Example: @.golden/sort-scaling-aarch64-darwin-Apple_M1.csv@
getSweepCSVPath :: FilePath -> Text -> String -> FilePath
getSweepCSVPath :: FilePath -> Text -> FilePath -> FilePath
getSweepCSVPath FilePath
outDir Text
archId FilePath
sweepName =
  let sanitizedName :: FilePath
sanitizedName = Text -> FilePath
T.unpack (Text -> FilePath) -> Text -> FilePath
forall a b. (a -> b) -> a -> b
$ Text -> Text
sanitizeForFilename (FilePath -> Text
T.pack FilePath
sweepName)
      sanitizedArch :: FilePath
sanitizedArch = Text -> FilePath
T.unpack (Text -> FilePath) -> Text -> FilePath
forall a b. (a -> b) -> a -> b
$ Text -> Text
sanitizeForFilename Text
archId
  in FilePath
outDir FilePath -> FilePath -> FilePath
</> (FilePath
sanitizedName FilePath -> FilePath -> FilePath
forall a. Semigroup a => a -> a -> a
<> FilePath
"-" FilePath -> FilePath -> FilePath
forall a. Semigroup a => a -> a -> a
<> FilePath
sanitizedArch) FilePath -> FilePath -> FilePath
<.> FilePath
"csv"

-- | Write sweep results to a CSV file.
--
-- This creates the output directory if it doesn't exist and writes
-- the complete CSV content atomically.
writeSweepCSV :: FilePath -> Text -> String -> Text -> [(Text, GoldenStats)] -> IO ()
writeSweepCSV :: FilePath
-> Text -> FilePath -> Text -> [(Text, GoldenStats)] -> IO ()
writeSweepCSV FilePath
outDir Text
archId FilePath
sweepName Text
paramName [(Text, GoldenStats)]
rows = do
  Bool -> FilePath -> IO ()
createDirectoryIfMissing Bool
True FilePath
outDir
  let path :: FilePath
path = FilePath -> Text -> FilePath -> FilePath
getSweepCSVPath FilePath
outDir Text
archId FilePath
sweepName
      content :: Text
content = Builder -> Text
B.toLazyText (Builder -> Text) -> Builder -> Text
forall a b. (a -> b) -> a -> b
$ Text -> [(Text, GoldenStats)] -> Builder
buildCSV Text
paramName [(Text, GoldenStats)]
rows
  FilePath -> Text -> IO ()
TL.writeFile FilePath
path Text
content