packages feed

warlock-0.1.0.0: src/Warlock.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE ScopedTypeVariables #-}
-- |
-- Module      : Warlock
-- Description : Unified Template Haskell derivation for Witch From/TryFrom instances
-- License     : MIT
-- Maintainer  : ian@iankduncan.com
-- Stability   : experimental
--
-- = Overview
--
-- Unified Template Haskell derivation supporting both structural (positional)
-- and semantic (name-based) mapping strategies.
--
-- = Quick Start
--
-- @
-- import Warlock
--
-- -- Structural mapping (by position):
-- deriveAutomap ByPosition ''Foo ''Either
--
-- -- Semantic mapping (by name):
-- deriveAutomap (ByName datatypePrefixConfig) ''Person ''Employee
--
-- -- With custom configuration:
-- deriveAutomap (ByName $ datatypePrefixConfig \`withDefaults\` [(\"newField\", [| 0 |])])
--               ''Source ''Dest
-- @
--
-- = Matching Strategies
--
-- [@ByPosition@]: Structural matching by declaration order.
-- Constructors: 1st → 1st, 2nd → 2nd. Fields: always positional.
-- Use for: Types with same shape but different names (`Either`, `Maybe`, etc.)
--
-- [@ByName@]: Semantic matching with configurable rules.
-- Constructors: matched by name (case-insensitive).
-- Fields: matched by name with prefix rules, defaults, computed fields.
-- Use for: Types with naming conventions (`personName` ↔ `employeeName`)
--
-- = Configuration
--
-- Common configs:
--
-- * 'datatypePrefixConfig' - Handle `personName` ↔ `employeeName` (most common)
-- * 'constructorPrefixConfig' - Handle constructor-prefixed fields
-- * 'defaultConfig' - No automatic rules (exact name matching)
--
-- Field-level customization:
--
-- * 'withDefaults' - Provide default values for new fields
-- * 'withRenames' - Rename specific fields
-- * 'withRules' - Add custom field rules (computed, virtual, disassemble)
--
-- = Examples
--
-- @
-- -- Simple structural mapping:
-- data Result = Success String | Failure Int
-- data HttpStatus = Ok String | Error Int
-- deriveAutomap ByPosition ''Result ''HttpStatus
--
-- -- Name-based with automatic prefix handling:
-- data Person = Person { personName :: String, personAge :: Int }
-- data Employee = Employee { employeeName :: String, employeeAge :: Int }
-- deriveAutomap (ByName datatypePrefixConfig) ''Person ''Employee
--
-- -- With defaults for new fields:
-- deriveAutomap (ByName $ datatypePrefixConfig \`withDefaults\`
--                [(\"employeeId\", [| 0 |])]
--               ) ''Person ''Employee
--
-- -- Combining multiple source fields:
-- deriveAutomap (ByName $ defaultConfig \`withRules\`
--                [ combineFields \"fullName\" $ do
--                    firstName <- get \"firstName\"
--                    lastName <- get \"lastName\"
--                    pure [| $firstName ++ \" \" ++ $lastName |]
--                ]
--               ) ''PersonV1 ''PersonV2
-- @
module Warlock
  ( -- * Main Derivation API
    MatchStrategy(..)
  , deriveAutomap
  , deriveAutomapWith
  , deriveTryAutomap
  , deriveAutomapBoth
  , deriveTryAutomapBoth

    -- * Configuration
  , Config
  , AutoMapConfig(..)  -- Export constructor for record syntax
  , datatypePrefixConfig
  , constructorPrefixConfig
  , conventionalConfig
  , snakeToCamelConfig
  , camelToSnakeConfig
  , defaultConfig

    -- * Configuration Helpers
  , withDefaults
  , withRenames
  , withRules
  , withConstructorMap
  , withNormalize
  , withRuleGens

    -- * Constructor Mapping
  , ConstructorMapping(..)
  , ToConstructorMapping(..)
  , stripSuffix
  , addSuffix
  , replaceSuffix

    -- * Field Rules (for ByName strategy)
  , FieldRule(..)
  , FieldRef(..)
  , ToFieldRef(..)
  , field
  , fromField
  , withDefault
  , withExpr
  , asVirtual
  , withComputed

    -- * Pre-composed Field Rules
  , rename
  , defaultTo
  , ignore
  , mapField
  , virtualField
  , computedField
  , combineFields
  , disassembleFields
  , DisassembledField(..)
  , (.=)

    -- * Disassemble Field Getters
  , DisassembleGetter
  , getSource
  , getSourceName

    -- * Field Getters (for computed fields)
  , FieldGetter
  , get
  , getName

    -- * Field Rule Generators
  , datatypePrefixRules
  , constructorPrefixRules
  , customPrefixRules
  , stripConstructorPrefix
  , addConstructorPrefix
  , RuleGenContext(..)

    -- * Name Normalizers
  , snakeToCamel
  , camelToSnake
  , stripPrefix
  , lowerFirst

    -- * Conversion Helpers
  , automap
  , tryAutomap
  , automapEach
  , tryAutomapEach

    -- * Internal (for Warlock.Tweak and advanced use)
  , resolveFieldRulesAndBuildInstance
  , resolveFieldRulesAndBuildInstanceWithType

    -- * Re-exports from Witch
  , module Witch
  ) where

import Data.Function ((&))
import qualified Witch as W
import Witch (From(..), TryFrom(..), TryFromException(..))
import Language.Haskell.TH
import Control.Monad (forM, when)
import Control.Applicative ((<|>))
import Data.List (find)
import Data.Char (toLower)
import GHC.Records (HasField(..))
import GHC.TypeLits (KnownSymbol, symbolVal)
import GHC.OverloadedLabels (IsLabel(..))
import Data.Proxy (Proxy(..))

--------------------------------------------------------------------------------
-- Strategy Type

-- | Strategy for matching constructors and fields during derivation.
data MatchStrategy
  = ByPosition
    -- ^ Match structurally by position.
    -- Constructors: 1st → 1st, 2nd → 2nd (must have same count).
    -- Fields: always positional (must have same arity per constructor).
  | ByName Config
    -- ^ Match by name with configurable rules.
    -- Constructors: by name (case-insensitive).
    -- Fields: by name with prefix rules, defaults, computed fields, etc.

-- | Configuration alias for cleaner API
type Config = AutoMapConfig

--------------------------------------------------------------------------------
-- Field Reference Types

-- | A reference to a field, either real (compile-time checked) or virtual (runtime via HasField).
--
-- Use:
-- - 'RealField' with TH names: @'fieldName@ for compile-time safety
-- - 'VirtualField' with strings or #labels: @#fieldName@ or @\"fieldName\"@ for HasField lookups
data FieldRef
  = RealField Name      -- ^ Real field that exists in the data type
  | VirtualField String -- ^ Virtual field accessed via HasField at runtime
  deriving (Show)

-- | Type class for converting various representations to FieldRef
class ToFieldRef a where
  toFieldRef :: a -> FieldRef

-- | TH Names become real fields
instance ToFieldRef Name where
  toFieldRef = RealField

-- | Strings become virtual fields
instance ToFieldRef String where
  toFieldRef = VirtualField

-- | OverloadedLabels support: #fieldName becomes a virtual field
instance (KnownSymbol s) => IsLabel s FieldRef where
  fromLabel = VirtualField (symbolVal (Proxy @s))

-- | Extract the string representation of a FieldRef (for normalization)
fieldRefToString :: FieldRef -> String
fieldRefToString (RealField n) = nameBase n
fieldRefToString (VirtualField s) = s

--------------------------------------------------------------------------------
-- Constructor Mapping Types

-- | Strategy for mapping constructor names during derivation.
--
-- Use:
-- - 'Identity' - no transformation (1:1 mapping by name)
-- - 'DirectMapping' - explicit constructor mappings: @[('OldCon, 'NewCon)]@
-- - 'Transform' - custom function: @Transform (\s -> s ++ "V2")@
-- - Helper combinators: 'stripSuffix', 'addSuffix', 'replaceSuffix'
data ConstructorMapping
  = Identity                           -- ^ No transformation (constructor names match)
  | DirectMapping [(Name, Name)]       -- ^ Explicit constructor name mappings (compile-time checked)
  | Transform (String -> String)       -- ^ Custom transformation function

-- | Type class for converting to ConstructorMapping
class ToConstructorMapping a where
  toConstructorMapping :: a -> ConstructorMapping

-- | Identity function becomes Identity mapping
instance ToConstructorMapping () where
  toConstructorMapping () = Identity

-- | String transformations become Transform
instance ToConstructorMapping (String -> String) where
  toConstructorMapping = Transform

-- | List of Name pairs becomes DirectMapping
instance ToConstructorMapping [(Name, Name)] where
  toConstructorMapping = DirectMapping

-- | ConstructorMapping is already a ConstructorMapping
instance ToConstructorMapping ConstructorMapping where
  toConstructorMapping = id

-- | Apply a constructor mapping to a constructor name
applyConstructorMapping :: ConstructorMapping -> String -> String
applyConstructorMapping Identity s = s
applyConstructorMapping (DirectMapping mappings) s =
  -- Case-insensitive lookup since constructor matching is case-insensitive
  case find (\(srcName, _) -> map toLower (nameBase srcName) == map toLower s) mappings of
    Just (_, targetName) -> nameBase targetName
    Nothing -> s  -- Fall back to identity if not in mapping
applyConstructorMapping (Transform f) s = f s

-- | Helper: Strip a suffix from constructor names
--
-- Example: @stripSuffix "V1"@ transforms @FooV1@ to @Foo@
stripSuffix :: String -> ConstructorMapping
stripSuffix suffix = Transform $ \s ->
  case reverse s of
    revS | reverse suffix == take (length suffix) revS ->
           reverse (drop (length suffix) revS)
    _ -> s

-- | Helper: Add a suffix to constructor names
--
-- Example: @addSuffix "V2"@ transforms @Foo@ to @FooV2@
addSuffix :: String -> ConstructorMapping
addSuffix suffix = Transform (++ suffix)

-- | Helper: Replace one suffix with another
--
-- Example: @replaceSuffix "V1" "V2"@ transforms @FooV1@ to @FooV2@
replaceSuffix :: String -> String -> ConstructorMapping
replaceSuffix oldSuffix newSuffix = Transform $ \s ->
  case reverse s of
    revS | reverse oldSuffix == take (length oldSuffix) revS ->
           reverse (drop (length oldSuffix) revS) ++ newSuffix
    _ -> s ++ newSuffix  -- If old suffix not found, just add new one

--------------------------------------------------------------------------------
-- Conversion Functions (re-exports from witch)

-- | Alias for 'W.from' - total conversion
automap :: W.From a b => a -> b
automap = W.from

-- | Alias for 'W.tryFrom' - fallible conversion
tryAutomap :: W.TryFrom a b => a -> Either (W.TryFromException a b) b
tryAutomap = W.tryFrom

-- | Map a conversion over a list
automapEach :: W.From a b => [a] -> [b]
automapEach = map W.from

-- | Try to map a conversion over a list, collecting results
tryAutomapEach :: W.TryFrom a b => [a] -> [Either (W.TryFromException a b) b]
tryAutomapEach = map W.tryFrom

--------------------------------------------------------------------------------
-- Main Derivation API

-- | Derive a 'From' instance using the specified matching strategy.
--
-- @
-- -- Structural:
-- deriveAutomap ByPosition ''Foo ''Bar
--
-- -- Semantic:
-- deriveAutomap (ByName datatypePrefixConfig) ''Person ''Employee
-- @
deriveAutomap :: MatchStrategy -> Name -> Name -> Q [Dec]
deriveAutomap ByPosition = deriveByPosition
deriveAutomap (ByName config) = deriveByName config

-- | Derive a 'From' instance for parameterized types.
--
-- This allows deriving instances for applied type constructors like @Container Int@.
--
-- @
-- deriveAutomapWith (ByName datatypePrefixConfig) [t| Container Int |] [t| IntContainer |]
-- @
deriveAutomapWith :: MatchStrategy -> Q Type -> Q Type -> Q [Dec]
deriveAutomapWith ByPosition srcTypeQ dstTypeQ =
  deriveWithTypePositional False srcTypeQ dstTypeQ
deriveAutomapWith (ByName config) srcTypeQ dstTypeQ =
  deriveWithTypeByName config srcTypeQ dstTypeQ

-- | Derive a 'TryFrom' instance using the specified matching strategy.
deriveTryAutomap :: MatchStrategy -> Name -> Name -> Q [Dec]
deriveTryAutomap ByPosition = deriveTryByPosition
deriveTryAutomap (ByName config) = deriveTryByName config

-- | Derive both directions: @From a b@ and @From b a@
deriveAutomapBoth :: MatchStrategy -> Name -> Name -> Q [Dec]
deriveAutomapBoth strategy srcName dstName = do
  forward <- deriveAutomap strategy srcName dstName
  backward <- deriveAutomap strategy dstName srcName
  pure (forward ++ backward)

-- | Derive both directions: @TryFrom a b@ and @TryFrom b a@
deriveTryAutomapBoth :: MatchStrategy -> Name -> Name -> Q [Dec]
deriveTryAutomapBoth strategy srcName dstName = do
  forward <- deriveTryAutomap strategy srcName dstName
  backward <- deriveTryAutomap strategy dstName srcName
  pure (forward ++ backward)

--------------------------------------------------------------------------------
-- Configuration Types (for ByName strategy)

-- | A rule for how to populate a destination field.
--
-- Use the builder functions ('field', 'from', 'withDefault', etc.) to construct rules.
--
-- Examples:
--   * @rename "balance" "balance_cents"@
--   * @defaultTo "region" [| "US" :: String |]@
--   * @mapField "name" (\srcF dstF -> [| $(varE srcF) ++ "!" |])@
data FieldRule = FieldRule
  { dst      :: Name
  , src      :: Maybe FieldRef  -- Can be real or virtual field
  , default_ :: Maybe (Q Exp)
  , expr     :: Maybe (Name -> Name -> Q Exp)
  , virtual  :: Bool
  , computed :: Maybe ([(String, Name)] -> Q Exp)
  }

-- | Context information passed to field rule generators.
data RuleGenContext = RuleGenContext
  { sourceTypeName        :: Name
  , sourceConstructorName :: Name
  , destinationTypeName   :: Name
  , destinationConstructorName :: Name
  , normalizeFunction     :: String -> String
  }

-- | Configuration for name-based derivation.
--
-- Common patterns:
--
-- @
-- -- Version suffixes for enums:
-- defaultConfig { constructorMap = (++ "V2") }
--
-- -- Field prefix rules:
-- defaultConfig { ruleGens = [datatypePrefixRules] }
--
-- -- Custom field transformations:
-- defaultConfig
--   { normalize = snakeToCamel
--   , staticRules = [defaultTo "newField" [| "default" |]]
--   }
-- @
data AutoMapConfig = AutoMapConfig
  { normalize          :: String -> String
  , ruleGens           :: [RuleGenContext -> String -> Maybe FieldRule]
  , staticRules        :: [FieldRule]
  , constructorMap     :: ConstructorMapping
  }

--------------------------------------------------------------------------------
-- Pre-defined Configurations

-- | Basic config: identity normalization, no rules, identity constructor mapping.
defaultConfig :: Config
defaultConfig = AutoMapConfig id [] [] Identity

-- | Config for Haskell datatype naming convention: fields prefixed by type name
--
-- Example: @Person { personName :: String }@ ↔ @Employee { employeeName :: String }@
datatypePrefixConfig :: Config
datatypePrefixConfig = defaultConfig { ruleGens = [datatypePrefixRules] }

-- | Config for constructor-prefixed fields
constructorPrefixConfig :: Config
constructorPrefixConfig = defaultConfig { ruleGens = [constructorPrefixRules] }

-- | Config that tries both datatype and constructor prefix conventions
conventionalConfig :: Config
conventionalConfig = defaultConfig { ruleGens = [datatypePrefixRules, constructorPrefixRules] }

-- | Config for snake_case → camelCase field name normalization
snakeToCamelConfig :: Config
snakeToCamelConfig = defaultConfig { normalize = snakeToCamel }

-- | Config for camelCase → snake_case field name normalization
camelToSnakeConfig :: Config
camelToSnakeConfig = defaultConfig { normalize = camelToSnake }

--------------------------------------------------------------------------------
-- Configuration Helpers

-- | Add defaults to a config
--
-- Example: @withDefaults config [("cvv", [| "000" |]), ("currency", [| "USD" |])]@
withDefaults :: Config -> [(Name, Q Exp)] -> Config
withDefaults cfg defaults = cfg { staticRules = staticRules cfg ++ map (uncurry defaultTo) defaults }

-- | Add renames to a config
--
-- Example: @withRenames config [(''newName, ''oldName)]@  (real fields)
-- Example: @withRenames config [(''empName, #fullName)]@  (virtual field)
withRenames :: ToFieldRef a => Config -> [(Name, a)] -> Config
withRenames cfg renames = cfg { staticRules = staticRules cfg ++ map (uncurry rename) renames }

-- | Add custom rules to a config
--
-- Example: @withRules config [virtualField "name" "fullName"]@
withRules :: Config -> [FieldRule] -> Config
withRules cfg rules = cfg { staticRules = staticRules cfg ++ rules }

-- | Set the constructor name mapping strategy
--
-- Examples:
--   @withConstructorMap config (addSuffix \"V2\")@
--   @withConstructorMap config [(''OldCon, ''NewCon)]@
--   @withConstructorMap config (\\s -> s ++ \"V2\")@
withConstructorMap :: ToConstructorMapping a => Config -> a -> Config
withConstructorMap cfg mapping = cfg { constructorMap = toConstructorMapping mapping }

-- | Set the field name normalization function
--
-- Example: @withNormalize config snakeToCamel@
withNormalize :: Config -> (String -> String) -> Config
withNormalize cfg f = cfg { normalize = f }

-- | Set the rule generators
--
-- Example: @withRuleGens config [datatypePrefixRules, customPrefixRules "foo" "bar"]@
withRuleGens :: Config -> [RuleGenContext -> String -> Maybe FieldRule] -> Config
withRuleGens cfg gens = cfg { ruleGens = gens }

--------------------------------------------------------------------------------
-- Field Rule Builders

-- | Create a base field rule for a destination field (use modifiers to customize)
--
-- Example: @field "balance"@
field :: Name -> FieldRule
field dstField = FieldRule dstField Nothing Nothing Nothing False Nothing

-- | Specify the source field name (modifier)
--
-- Example: @field ''balance & fromField ''balance_cents@  (real field)
-- Example: @field ''name & fromField #fullName@  (virtual field via OverloadedLabels)
fromField :: ToFieldRef a => a -> FieldRule -> FieldRule
fromField srcField rule = rule { src = Just (toFieldRef srcField) }

-- | Provide a default value when source is missing (modifier)
--
-- Example: @field "region" & withDefault [| "US" |]@
withDefault :: Q Exp -> FieldRule -> FieldRule
withDefault defVal rule = rule { default_ = Just defVal }

-- | Use a custom expression to compute the field (modifier)
--
-- Example: @field "name" & withExpr (\src dst -> [| $(varE src) ++ "!" |])@
withExpr :: (Name -> Name -> Q Exp) -> FieldRule -> FieldRule
withExpr customExpr rule = rule { expr = Just customExpr }

-- | Mark field as virtual (accessed via HasField) (modifier)
asVirtual :: FieldRule -> FieldRule
asVirtual rule = rule { virtual = True }

-- | Use a computed expression that combines multiple fields (modifier)
withComputed :: ([(String, Name)] -> Q Exp) -> FieldRule -> FieldRule
withComputed computeExpr rule = rule { computed = Just computeExpr }

--------------------------------------------------------------------------------
-- Pre-composed Field Rules

-- | Rename a field
rename :: ToFieldRef a => Name -> a -> FieldRule
rename dstField srcField = field dstField & fromField srcField

-- | Provide a default value for a destination field
defaultTo :: Name -> Q Exp -> FieldRule
defaultTo dstField defVal = field dstField & withDefault defVal

-- | Ignore a destination field
ignore :: Name -> FieldRule
ignore dstField = field dstField & withDefault [| error "ignored field" |]

-- | Map a field with a custom expression
mapField :: Name -> (Name -> Name -> Q Exp) -> FieldRule
mapField dstField customExpr = field dstField & withExpr customExpr

-- | Create a virtual field rule (uses HasField at runtime)
--
-- Example: @virtualField ''empName #fullName@ (using OverloadedLabels)
-- Example: @virtualField ''empName \"fullName\"@ (using String)
virtualField :: ToFieldRef a => Name -> a -> FieldRule
virtualField dstField srcField = field dstField & fromField srcField & asVirtual

-- | Create a computed field rule (compile-time)
computedField :: Name -> Q Exp -> FieldRule
computedField dstField expr =
  FieldRule dstField Nothing Nothing (Just $ \_ _ -> expr) False Nothing

-- | Monad for safely extracting field variables in 'combineFields'
newtype FieldGetter a = FieldGetter { runFieldGetter :: [(String, Name)] -> Either String (a, [String]) }

instance Functor FieldGetter where
  fmap f (FieldGetter g) = FieldGetter $ \ctx -> case g ctx of
    Left err -> Left err
    Right (a, used) -> Right (f a, used)

instance Applicative FieldGetter where
  pure x = FieldGetter $ \_ -> Right (x, [])
  FieldGetter ff <*> FieldGetter fa = FieldGetter $ \ctx -> case ff ctx of
    Left err -> Left err
    Right (f, used1) -> case fa ctx of
      Left err -> Left err
      Right (a, used2) -> Right (f a, used1 ++ used2)

instance Monad FieldGetter where
  FieldGetter m >>= f = FieldGetter $ \ctx -> case m ctx of
    Left err -> Left err
    Right (a, used1) -> case runFieldGetter (f a) ctx of
      Left err -> Left err
      Right (b, used2) -> Right (b, used1 ++ used2)

-- | Extract a field variable as a Template Haskell expression
get :: Name -> FieldGetter (Q Exp)
get fieldName = FieldGetter $ \fieldVarMap ->
  let fieldNameStr = nameBase fieldName
  in case lookup fieldNameStr fieldVarMap of
       Just var -> Right (varE var, [fieldNameStr])
       Nothing -> Left $ "Field '" ++ fieldNameStr ++ "' not found in source type"

-- | Extract a field variable as a Name
getName :: Name -> FieldGetter Name
getName fName = FieldGetter $ \fieldVarMap ->
  let fieldNameStr = nameBase fName
  in case lookup fieldNameStr fieldVarMap of
       Just var -> Right (var, [fieldNameStr])
       Nothing -> Left $ "Field '" ++ fieldNameStr ++ "' not found in source type"

-- | Combine multiple source fields into a destination field
combineFields :: Name -> FieldGetter (Q Exp) -> FieldRule
combineFields dstField getter =
  FieldRule dstField Nothing Nothing Nothing False (Just mkComputed)
  where
    mkComputed fieldVarMap = case runFieldGetter getter fieldVarMap of
      Left err -> fail $ "combineFields for '" ++ nameBase dstField ++ "': " ++ err
      Right (expr, _usedFields) -> expr

-- | Monad for accessing the source field in 'disassembleFields'
newtype DisassembleGetter a = DisassembleGetter { runDisassembleGetter :: Name -> a }

instance Functor DisassembleGetter where
  fmap f (DisassembleGetter g) = DisassembleGetter (f . g)

instance Applicative DisassembleGetter where
  pure x = DisassembleGetter (const x)
  DisassembleGetter f <*> DisassembleGetter x = DisassembleGetter (\n -> f n (x n))

instance Monad DisassembleGetter where
  DisassembleGetter f >>= k = DisassembleGetter $ \n ->
    runDisassembleGetter (k (f n)) n

-- | Get the source field as a Template Haskell expression
--
-- Example: @src <- getSource; pure [| words $src |]@
getSource :: DisassembleGetter (Q Exp)
getSource = DisassembleGetter varE

-- | Get the source field as a Name (for advanced use cases)
--
-- Example: @srcName <- getSourceName; pure [| $(varE srcName) |]@
getSourceName :: DisassembleGetter Name
getSourceName = DisassembleGetter id

-- | A field assignment for disassembled fields
data DisassembledField = DisassembledField Name (DisassembleGetter (Q Exp))

-- | Infix operator for creating disassembled field assignments
--
-- Example:
-- @
-- 'firstName .= do
--   src <- getSource
--   pure [| case words $src of (f:_) -> f; _ -> "" |]
-- @
(.=) :: Name -> DisassembleGetter (Q Exp) -> DisassembledField
(.=) = DisassembledField
infixr 0 .=

-- | Disassemble one source field into multiple destination fields
--
-- Example:
-- @
-- disassembleFields 'fullName
--   [ 'firstName .= do
--       src <- getSource
--       pure [| case words $src of (f:_) -> f; _ -> "" |]
--   , 'lastName .= do
--       src <- getSource
--       pure [| case words $src of (_:l:_) -> l; _ -> "" |]
--   ]
-- @
disassembleFields :: ToFieldRef a => a -> [DisassembledField] -> [FieldRule]
disassembleFields srcField assignments =
  [ FieldRule dstField (Just (toFieldRef srcField)) Nothing (Just mkExpr) False Nothing
  | DisassembledField dstField getter <- assignments
  , let mkExpr srcVarName _dstVarName = runDisassembleGetter getter srcVarName
  ]

--------------------------------------------------------------------------------
-- Field Rule Generators

-- | Strip a prefix from a string
stripPrefix :: String -> String -> Maybe String
stripPrefix [] ys = Just ys
stripPrefix _ [] = Nothing
stripPrefix (x:xs) (y:ys)
  | x == y    = stripPrefix xs ys
  | otherwise = Nothing

-- | Lowercase the first character
lowerFirst :: String -> String
lowerFirst [] = []
lowerFirst (c:cs) = toLower' c : cs
  where toLower' ch = if ch >= 'A' && ch <= 'Z' then toEnum (fromEnum ch + 32) else ch

-- | Generate field rules for datatypes following "prefix fields with datatype name" convention
datatypePrefixRules :: RuleGenContext -> String -> Maybe FieldRule
datatypePrefixRules RuleGenContext{..} dstField = do
  let dstTypeName = lowerFirst $ nameBase destinationTypeName
      srcTypeName = lowerFirst $ nameBase sourceTypeName
  suffix <- stripPrefix dstTypeName dstField
  let srcField = srcTypeName ++ suffix
  pure $ FieldRule (mkName dstField) (Just (RealField (mkName srcField))) Nothing Nothing False Nothing

-- | Generate field rules for datatypes following "prefix fields with constructor name" convention
constructorPrefixRules :: RuleGenContext -> String -> Maybe FieldRule
constructorPrefixRules RuleGenContext{..} dstField = do
  let dstConName = lowerFirst $ nameBase destinationConstructorName
      srcConName = lowerFirst $ nameBase sourceConstructorName
  suffix <- stripPrefix dstConName dstField
  let srcField = srcConName ++ suffix
  pure $ FieldRule (mkName dstField) (Just (RealField (mkName srcField))) Nothing Nothing False Nothing

-- | Generate field rules for custom prefix transformations
customPrefixRules :: String -> String -> RuleGenContext -> String -> Maybe FieldRule
customPrefixRules dstPrefix srcPrefix _ctx dstField = do
  suffix <- stripPrefix dstPrefix dstField
  let srcField = srcPrefix ++ suffix
  pure $ FieldRule (mkName dstField) (Just (RealField (mkName srcField))) Nothing Nothing False Nothing

-- | Strip constructor prefix from destination fields
stripConstructorPrefix :: String -> RuleGenContext -> String -> Maybe FieldRule
stripConstructorPrefix prefix _ctx dstField = do
  suffix <- stripPrefix prefix dstField
  pure $ FieldRule (mkName dstField) (Just (RealField (mkName suffix))) Nothing Nothing False Nothing

-- | Add constructor prefix to source field lookups
addConstructorPrefix :: String -> RuleGenContext -> String -> Maybe FieldRule
addConstructorPrefix prefix _ctx dstField =
  let srcField = prefix ++ dstField
  in Just $ FieldRule (mkName dstField) (Just (RealField (mkName srcField))) Nothing Nothing False Nothing

--------------------------------------------------------------------------------
-- Name Normalizers

-- | Convert snake_case to camelCase
snakeToCamel :: String -> String
snakeToCamel = go True
  where
    go _ [] = []
    go _ ('_':cs) = go True cs
    go True (c:cs) = toEnum (fromEnum c - 32) : go False cs
    go _    (c:cs) = c : go False cs

-- | Convert camelCase to snake_case
camelToSnake :: String -> String
camelToSnake = concatMap step
  where
    step c | c >= 'A' && c <= 'Z' = ['_', toEnum (fromEnum c + 32)]
           | otherwise            = [c]

--------------------------------------------------------------------------------
-- Constructor Information

-- | Represents a constructor's fields
data ConstructorInfo
  = RecordConstructor Name [(Name, Bang, Type)]
  | PositionalConstructor Name [(Bang, Type)]
  deriving (Show)

-- | Extract constructors from a type
expectConstructors :: Name -> Q [ConstructorInfo]
expectConstructors tyName = reify tyName >>= \case
  TyConI (DataD _ _ _ _ cons _) -> mapM extractCon cons
  TyConI (NewtypeD _ _ _ _ con _) -> (:[]) <$> extractCon con
  _ -> fail $ "Witch.TH: type " <> show tyName <> " must be a data type"
  where
    extractCon (RecC conName fields) = pure $ RecordConstructor conName fields
    extractCon (NormalC conName fields) = pure $ PositionalConstructor conName fields
    extractCon (InfixC t1 conName t2) = pure $ PositionalConstructor conName [t1, t2]
    extractCon _ = fail "Witch.TH: unsupported constructor type (GADTs, existentials not supported)"

-- | Get constructor name
getConstructorName :: ConstructorInfo -> Name
getConstructorName (RecordConstructor n _) = n
getConstructorName (PositionalConstructor n _) = n

-- | Build a map from normalized fieldName -> actual Name
mkFieldMap :: (String -> String) -> [(Name, Bang, Type)] -> [(String, Name)]
mkFieldMap norm fs = [ (norm (nameBase n), n) | (n,_,_) <- fs ]

--------------------------------------------------------------------------------
-- Positional (Structural) Derivation

deriveByPosition :: Name -> Name -> Q [Dec]
deriveByPosition = derivePositional False

deriveTryByPosition :: Name -> Name -> Q [Dec]
deriveTryByPosition = derivePositional True

derivePositional :: Bool -> Name -> Name -> Q [Dec]
derivePositional isTry srcName dstName = do
  srcCons <- getConstructorsSimple srcName
  dstCons <- getConstructorsSimple dstName

  when (length srcCons /= length dstCons) $
    fail $ "Witch.TH.ByPosition: constructor count mismatch: "
        <> show srcName <> " has " <> show (length srcCons) <> " constructors, "
        <> show dstName <> " has " <> show (length dstCons) <> " constructors"

  sVar <- newName "s"
  matches <- forM (zip srcCons dstCons) $ \(srcCon, dstCon) ->
    derivePositionalCase isTry srcCon dstCon

  let fromMethod = if isTry then 'W.tryFrom else 'W.from
      fromBody = caseE (varE sVar) (map pure matches)

  if isTry
    then instanceD (pure []) [t| TryFrom $(conT srcName) $(conT dstName) |]
           [ funD fromMethod [clause [varP sVar] (normalB fromBody) []] ] >>= pure . (:[])
    else instanceD (pure []) [t| From $(conT srcName) $(conT dstName) |]
           [ funD fromMethod [clause [varP sVar] (normalB fromBody) []] ] >>= pure . (:[])

type SimpleConstructorInfo = (Name, Int)

getConstructorsSimple :: Name -> Q [SimpleConstructorInfo]
getConstructorsSimple tyName = reify tyName >>= \case
  TyConI (DataD _ _ _ _ cons _) -> mapM extractConInfo cons
  TyConI (NewtypeD _ _ _ _ con _) -> (:[]) <$> extractConInfo con
  _ -> fail $ "Witch.TH: " <> show tyName <> " must be a data type"
  where
    extractConInfo (NormalC conName fields) = pure (conName, length fields)
    extractConInfo (RecC conName fields) = pure (conName, length fields)
    extractConInfo (InfixC _ conName _) = pure (conName, 2)
    extractConInfo _ = fail "Witch.TH: unsupported constructor type"

derivePositionalCase :: Bool -> SimpleConstructorInfo -> SimpleConstructorInfo -> Q Match
derivePositionalCase isTry (srcConName, srcArity) (dstConName, dstArity) = do
  when (srcArity /= dstArity) $
    fail $ "Witch.TH.ByPosition: arity mismatch: "
        <> show srcConName <> " has " <> show srcArity <> " fields, "
        <> show dstConName <> " has " <> show dstArity <> " fields"

  srcVars <- mapM (\i -> newName ("x" ++ show i)) [1..srcArity]
  let pat = ConP srcConName [] (map VarP srcVars)
      convertVar v = if isTry then [| W.tryFrom $(varE v) |] else [| W.from $(varE v) |]

  if isTry && srcArity > 0
    then do
      let applyAll = foldl (\acc v -> [| $acc <*> $(convertVar v) |])
                           [| pure $(conE dstConName) |]
                           srcVars
      match (pure pat) (normalB applyAll) []
    else do
      conversions <- mapM convertVar srcVars
      let bodyExp = foldl AppE (ConE dstConName) conversions
      match (pure pat) (normalB (pure bodyExp)) []

-- Positional derivation for parameterized types
deriveWithTypePositional :: Bool -> Q Type -> Q Type -> Q [Dec]
deriveWithTypePositional isTry srcTypeQ dstTypeQ = do
  srcType <- srcTypeQ
  dstType <- dstTypeQ

  let getBaseName :: Type -> Name
      getBaseName (ConT name) = name
      getBaseName (AppT t _) = getBaseName t
      getBaseName t = error $ "deriveWithType: unsupported type structure: " ++ show t

      srcName = getBaseName srcType
      dstName = getBaseName dstType

  srcCons <- getConstructorsSimple srcName
  dstCons <- getConstructorsSimple dstName

  when (length srcCons /= length dstCons) $
    fail $ "Witch.TH.ByPosition: constructor count mismatch"

  sVar <- newName "s"
  matches <- forM (zip srcCons dstCons) $ \(srcCon, dstCon) ->
    derivePositionalCase isTry srcCon dstCon

  let fromMethod = if isTry then 'W.tryFrom else 'W.from
      fromBody = caseE (varE sVar) (map pure matches)

  if isTry
    then instanceD (pure []) [t| TryFrom $srcTypeQ $dstTypeQ |]
           [ funD fromMethod [clause [varP sVar] (normalB fromBody) []] ] >>= pure . (:[])
    else instanceD (pure []) [t| From $srcTypeQ $dstTypeQ |]
           [ funD fromMethod [clause [varP sVar] (normalB fromBody) []] ] >>= pure . (:[])

--------------------------------------------------------------------------------
-- Name-Based (Semantic) Derivation

deriveByName :: Config -> Name -> Name -> Q [Dec]
deriveByName config srcName dstName = do
  srcCons <- expectConstructors srcName
  dstCons <- expectConstructors dstName

  case (srcCons, dstCons) of
    ([src], [dst]) -> deriveSingleConstructor config srcName dstName src dst
    _ -> deriveMultiConstructor config srcName dstName srcCons dstCons

-- Name-based derivation for parameterized types
deriveWithTypeByName :: Config -> Q Type -> Q Type -> Q [Dec]
deriveWithTypeByName config srcTypeQ dstTypeQ = do
  srcType <- srcTypeQ
  dstType <- dstTypeQ

  let getBaseName :: Type -> Name
      getBaseName (ConT name) = name
      getBaseName (AppT t _) = getBaseName t
      getBaseName t = error $ "deriveWithType: unsupported type structure: " ++ show t

      srcName = getBaseName srcType
      dstName = getBaseName dstType

  srcCons <- expectConstructors srcName
  dstCons <- expectConstructors dstName

  case (srcCons, dstCons) of
    ([src], [dst]) -> deriveSingleConstructorWithType config srcTypeQ dstTypeQ srcName dstName src dst
    _ -> deriveMultiConstructorWithType config srcTypeQ dstTypeQ srcName dstName srcCons dstCons

deriveTryByName :: Config -> Name -> Name -> Q [Dec]
deriveTryByName _config srcName dstName = do
  -- Simple implementation - could be enhanced to use config
  srcCons <- expectConstructors srcName
  dstCons <- expectConstructors dstName

  case (srcCons, dstCons) of
    ([RecordConstructor _srcCon srcFields], [RecordConstructor dstCon dstFields]) -> do
      let srcMap = [ (nameBase n, n) | (n,_,_) <- srcFields ]
          findSrc nm = lookup nm srcMap

      sVar <- newName "s"
      fieldApps <- mapM (\(dstFName,_,_) -> do
          let key = nameBase dstFName
          case findSrc key of
            Just srcNm -> [| V (W.tryInto ($(varE srcNm) $(varE sVar)) :: Either String _) |]
            Nothing -> [| V (Left ["missing source field for '" <> key <> "'"]) |]
        ) dstFields

      let buildRec = do
            xs <- mapM (\_ -> newName "x") dstFields
            let pairs = zipWith (\(n,_,_) x -> (n, VarE x)) dstFields xs
            pure $ LamE (map VarP xs) (RecConE dstCon pairs)

      recBuilder <- buildRec
      let bodyExp = foldl (\acc v -> [| $(acc) <*> $(v) |]) [| pure $(pure recBuilder) |] (fmap pure fieldApps)
          body = normalB [| vToEither $ $(bodyExp) |]

      instanceD (pure []) [t| W.TryFrom $(conT srcName) $(conT dstName) |]
        [ funD 'W.tryFrom [clause [varP sVar] body []] ] >>= pure . (:[])

    _ -> fail "Witch.TH: TryFrom with ByName only supports single-constructor records for now"

-- Validation applicative for TryFrom
newtype V a = V { unV :: Either [String] a }

instance Functor V where
  fmap f (V e) = V (fmap f e)

instance Applicative V where
  pure = V . Right
  V (Left e1) <*> V (Left e2) = V (Left (e1 <> e2))
  V (Left e)  <*> _           = V (Left e)
  _           <*> V (Left e)  = V (Left e)
  V (Right f) <*> V (Right x) = V (Right (f x))

vToEither :: V a -> Either [String] a
vToEither = unV

deriveSingleConstructor :: Config -> Name -> Name -> ConstructorInfo -> ConstructorInfo -> Q [Dec]
deriveSingleConstructor config srcName dstName srcCon dstCon =
  case (srcCon, dstCon) of
    (RecordConstructor srcConName srcFields, RecordConstructor dstConName dstFields) ->
      deriveSingleRecordConstructor config srcName dstName srcConName srcFields dstConName dstFields

    (PositionalConstructor srcConName srcFields, PositionalConstructor dstConName dstFields) ->
      deriveSimplePositional srcName dstName srcConName srcFields dstConName dstFields

    _ -> fail "Witch.TH: cannot mix record and positional constructors"

deriveSingleConstructorWithType :: Config -> Q Type -> Q Type -> Name -> Name -> ConstructorInfo -> ConstructorInfo -> Q [Dec]
deriveSingleConstructorWithType config srcTypeQ dstTypeQ srcName dstName srcCon dstCon =
  case (srcCon, dstCon) of
    (RecordConstructor srcConName srcFields, RecordConstructor dstConName dstFields) ->
      deriveSingleRecordConstructorWithType config srcTypeQ dstTypeQ srcName dstName srcConName srcFields dstConName dstFields

    (PositionalConstructor srcConName srcFields, PositionalConstructor dstConName dstFields) ->
      deriveSimplePositionalWithType srcTypeQ dstTypeQ srcConName srcFields dstConName dstFields

    _ -> fail "Witch.TH: cannot mix record and positional constructors"

deriveSimplePositional :: Name -> Name -> Name -> [(Bang, Type)] -> Name -> [(Bang, Type)] -> Q [Dec]
deriveSimplePositional srcName dstName srcCon srcFields dstCon dstFields = do
  when (length srcFields /= length dstFields) $
    fail $ "Witch.TH: arity mismatch"

  srcVars <- mapM (\i -> newName ("x" ++ show i)) [1..length srcFields]
  let srcPat = ConP srcCon [] (map VarP srcVars)
      dstExp = foldl AppE (ConE dstCon) [AppE (VarE 'W.into) (VarE v) | v <- srcVars]

  instanceD (pure []) [t| W.From $(conT srcName) $(conT dstName) |]
    [ funD 'W.from [clause [pure srcPat] (normalB (pure dstExp)) []] ] >>= pure . (:[])

deriveSimplePositionalWithType :: Q Type -> Q Type -> Name -> [(Bang, Type)] -> Name -> [(Bang, Type)] -> Q [Dec]
deriveSimplePositionalWithType srcTypeQ dstTypeQ srcCon srcFields dstCon dstFields = do
  when (length srcFields /= length dstFields) $
    fail $ "Witch.TH: arity mismatch"

  srcVars <- mapM (\i -> newName ("x" ++ show i)) [1..length srcFields]
  let srcPat = ConP srcCon [] (map VarP srcVars)
      dstExp = foldl AppE (ConE dstCon) [AppE (VarE 'W.into) (VarE v) | v <- srcVars]

  instanceD (pure []) [t| W.From $srcTypeQ $dstTypeQ |]
    [ funD 'W.from [clause [pure srcPat] (normalB (pure dstExp)) []] ] >>= pure . (:[])

--------------------------------------------------------------------------------
-- Internal: Reusable Field Rule Resolution
--
-- These functions are used by both the main derive functions and Warlock.Tweak

-- | Resolve field rules and build a complete From instance for a record constructor.
-- This function must be atomic because TH newName generates unique IDs that can't be
-- reliably shared across function boundaries.
resolveFieldRulesAndBuildInstance
  :: Config
  -> Name  -- ^ Source type name
  -> Name  -- ^ Source constructor name
  -> [(Name, Bang, Type)]  -- ^ Source fields
  -> Name  -- ^ Destination type name
  -> Name  -- ^ Destination constructor name
  -> [(Name, Bang, Type)]  -- ^ Destination fields
  -> Q Dec  -- ^ Complete From instance
resolveFieldRulesAndBuildInstance AutoMapConfig{..} srcName srcCon srcFields dstName dstCon dstFields = do
  let srcMap = mkFieldMap normalize srcFields
      findSrc nm = lookup nm srcMap
      staticRulesByDst = [(normalize (nameBase (dst r)), r) | r <- staticRules]
      ruleGenCtx = RuleGenContext srcName srcCon dstName dstCon normalize
      tryGen [] _ = Nothing
      tryGen (g:gs) dstKey = g ruleGenCtx dstKey <|> tryGen gs dstKey

  srcVal <- newName "_srcVal"
  fieldVars <- mapM (\(fName, _, _) -> do
      vName <- newName ("v_" ++ nameBase fName)
      pure (fName, vName)) srcFields

  let fieldVarMap = [(nameBase fName, vName) | (fName, vName) <- fieldVars]

  assigns <- forM dstFields $ \(dstFName, _, _) -> do
    let dstKey  = normalize (nameBase dstFName)
        mbRule  = tryGen ruleGens dstKey <|> lookup dstKey staticRulesByDst
    e <- case mbRule of
      Just r | Just f <- computed r -> f fieldVarMap
             | Just f <- expr r -> do
                 let srcKey = case src r of
                       Just s -> normalize (fieldRefToString s)
                       Nothing -> dstKey
                 case lookup srcKey fieldVarMap of
                   Just srcVarName -> f srcVarName dstFName
                   Nothing -> fail $ "Witch.TH: expr references unknown source field '" <> srcKey <> "'"
             | virtual r -> do
                 let srcFieldName = maybe dstKey fieldRefToString (src r)
                     getFieldTyApp = AppTypeE (VarE 'getField) (LitT (StrTyLit srcFieldName))
                     getFieldApp = AppE getFieldTyApp (VarE srcVal)
                 [| W.into $(pure getFieldApp) |]
             | otherwise -> do
                 let srcKey = maybe dstKey (normalize . fieldRefToString) (src r)
                 case findSrc srcKey of
                   Just srcFName ->
                     case lookup (nameBase srcFName) fieldVarMap of
                       Just vName -> [| W.into $(varE vName) |]
                       Nothing -> fail $ "Internal error: field not in pattern"
                   Nothing -> case default_ r of
                     Just d  -> d
                     Nothing -> fail $ "Witch.TH: no source field for '" <> nameBase dstFName <> "'"
      Nothing ->
        case findSrc dstKey of
          Just srcFName ->
            case lookup (nameBase srcFName) fieldVarMap of
              Just vName -> [| W.into $(varE vName) |]
              Nothing -> fail $ "Internal error: field not in pattern"
          Nothing -> fail $ "Witch.TH: no source for destination field '" <> nameBase dstFName <> "'"
    pure (dstFName, e)

  -- Build the instance using the same variables
  let pat = AsP srcVal (RecP srcCon [(fName, VarP vName) | (fName, vName) <- fieldVars])
      bodyExp = RecConE dstCon assigns

  instanceD (pure []) [t| W.From $(conT srcName) $(conT dstName) |]
    [ funD 'W.from [clause [pure pat] (normalB (pure bodyExp)) []] ]

-- | Version for parameterized types
resolveFieldRulesAndBuildInstanceWithType
  :: Config
  -> Q Type  -- ^ Source type
  -> Name  -- ^ Source type name (for context)
  -> Name  -- ^ Source constructor name
  -> [(Name, Bang, Type)]  -- ^ Source fields
  -> Q Type  -- ^ Destination type
  -> Name  -- ^ Destination type name (for context)
  -> Name  -- ^ Destination constructor name
  -> [(Name, Bang, Type)]  -- ^ Destination fields
  -> Q Dec
resolveFieldRulesAndBuildInstanceWithType AutoMapConfig{..} srcTypeQ srcName srcCon srcFields dstTypeQ dstName dstCon dstFields = do
  let srcMap = mkFieldMap normalize srcFields
      findSrc nm = lookup nm srcMap
      staticRulesByDst = [(normalize (nameBase (dst r)), r) | r <- staticRules]
      ruleGenCtx = RuleGenContext srcName srcCon dstName dstCon normalize
      tryGen [] _ = Nothing
      tryGen (g:gs) dstKey = g ruleGenCtx dstKey <|> tryGen gs dstKey

  srcVal <- newName "_srcVal"
  fieldVars <- mapM (\(fName, _, _) -> do
      vName <- newName ("v_" ++ nameBase fName)
      pure (fName, vName)) srcFields

  let fieldVarMap = [(nameBase fName, vName) | (fName, vName) <- fieldVars]

  assigns <- forM dstFields $ \(dstFName, _, _) -> do
    let dstKey  = normalize (nameBase dstFName)
        mbRule  = tryGen ruleGens dstKey <|> lookup dstKey staticRulesByDst
    e <- case mbRule of
      Just r | Just f <- computed r -> f fieldVarMap
             | Just f <- expr r -> do
                 let srcKey = case src r of
                       Just s -> normalize (fieldRefToString s)
                       Nothing -> dstKey
                 case lookup srcKey fieldVarMap of
                   Just srcVarName -> f srcVarName dstFName
                   Nothing -> fail $ "Witch.TH: expr references unknown source field '" <> srcKey <> "'"
             | virtual r -> do
                 let srcFieldName = maybe dstKey fieldRefToString (src r)
                     getFieldTyApp = AppTypeE (VarE 'getField) (LitT (StrTyLit srcFieldName))
                     getFieldApp = AppE getFieldTyApp (VarE srcVal)
                 [| W.into $(pure getFieldApp) |]
             | otherwise -> do
                 let srcKey = maybe dstKey (normalize . fieldRefToString) (src r)
                 case findSrc srcKey of
                   Just srcFName ->
                     case lookup (nameBase srcFName) fieldVarMap of
                       Just vName -> [| W.into $(varE vName) |]
                       Nothing -> fail $ "Internal error: field not in pattern"
                   Nothing -> case default_ r of
                     Just d  -> d
                     Nothing -> fail $ "Witch.TH: no source field for '" <> nameBase dstFName <> "'"
      Nothing ->
        case findSrc dstKey of
          Just srcFName ->
            case lookup (nameBase srcFName) fieldVarMap of
              Just vName -> [| W.into $(varE vName) |]
              Nothing -> fail $ "Internal error: field not in pattern"
          Nothing -> fail $ "Witch.TH: no source for destination field '" <> nameBase dstFName <> "'"
    pure (dstFName, e)

  let pat = AsP srcVal (RecP srcCon [(fName, VarP vName) | (fName, vName) <- fieldVars])
      bodyExp = RecConE dstCon assigns

  instanceD (pure []) [t| W.From $srcTypeQ $dstTypeQ |]
    [ funD 'W.from [clause [pure pat] (normalB (pure bodyExp)) []] ]

-- | Helper for building a match case in multi-constructor derivations
resolveFieldRulesForMatch
  :: Config
  -> Name  -- ^ Source type name
  -> Name  -- ^ Source constructor name
  -> [(Name, Bang, Type)]  -- ^ Source fields
  -> Name  -- ^ Destination type name
  -> Name  -- ^ Destination constructor name
  -> [(Name, Bang, Type)]  -- ^ Destination fields
  -> Q Match
resolveFieldRulesForMatch AutoMapConfig{..} srcName srcCon srcFields dstName dstCon dstFields = do
  let srcMap = mkFieldMap normalize srcFields
      findSrc nm = lookup nm srcMap
      staticRulesByDst = [(normalize (nameBase (dst r)), r) | r <- staticRules]
      ruleGenCtx = RuleGenContext srcName srcCon dstName dstCon normalize
      tryGen [] _ = Nothing
      tryGen (g:gs) dstKey = g ruleGenCtx dstKey <|> tryGen gs dstKey

  srcVal <- newName "_srcVal"
  fieldVars <- mapM (\(fName, _, _) -> do
      vName <- newName ("v_" ++ nameBase fName)
      pure (fName, vName)) srcFields

  let fieldVarMap = [(nameBase fName, vName) | (fName, vName) <- fieldVars]

  assigns <- forM dstFields $ \(dstFName, _, _) -> do
    let dstKey  = normalize (nameBase dstFName)
        mbRule  = tryGen ruleGens dstKey <|> lookup dstKey staticRulesByDst
    e <- case mbRule of
      Just r | Just f <- computed r -> f fieldVarMap
             | Just f <- expr r -> do
                 let srcKey = case src r of
                       Just s -> normalize (fieldRefToString s)
                       Nothing -> dstKey
                 case lookup srcKey fieldVarMap of
                   Just srcVarName -> f srcVarName dstFName
                   Nothing -> fail $ "Witch.TH: expr references unknown source field '" <> srcKey <> "'"
             | virtual r -> do
                 let srcFieldName = maybe dstKey fieldRefToString (src r)
                     getFieldTyApp = AppTypeE (VarE 'getField) (LitT (StrTyLit srcFieldName))
                     getFieldApp = AppE getFieldTyApp (VarE srcVal)
                 [| W.into $(pure getFieldApp) |]
             | otherwise -> do
                 let srcKey = maybe dstKey (normalize . fieldRefToString) (src r)
                 case findSrc srcKey of
                   Just srcFName ->
                     case lookup (nameBase srcFName) fieldVarMap of
                       Just vName -> [| W.into $(varE vName) |]
                       Nothing -> fail $ "Internal error: field not in pattern"
                   Nothing -> case default_ r of
                     Just d  -> d
                     Nothing -> fail $ "Witch.TH: no source field for '" <> nameBase dstFName <> "'"
      Nothing ->
        case findSrc dstKey of
          Just srcFName ->
            case lookup (nameBase srcFName) fieldVarMap of
              Just vName -> [| W.into $(varE vName) |]
              Nothing -> fail $ "Internal error: field not in pattern"
          Nothing -> fail $ "Witch.TH: no source for destination field '" <> nameBase dstFName <> "'"
    pure (dstFName, e)

  let pat = AsP srcVal (RecP srcCon [(fName, VarP vName) | (fName, vName) <- fieldVars])
      bodyExp = RecConE dstCon assigns

  match (pure pat) (normalB (pure bodyExp)) []

deriveSingleRecordConstructor :: Config -> Name -> Name -> Name -> [(Name, Bang, Type)] -> Name -> [(Name, Bang, Type)] -> Q [Dec]
deriveSingleRecordConstructor config srcName dstName srcCon srcFields dstCon dstFields = do
  inst <- resolveFieldRulesAndBuildInstance config srcName srcCon srcFields dstName dstCon dstFields
  pure [inst]

deriveSingleRecordConstructorWithType :: Config -> Q Type -> Q Type -> Name -> Name -> Name -> [(Name, Bang, Type)] -> Name -> [(Name, Bang, Type)] -> Q [Dec]
deriveSingleRecordConstructorWithType config srcTypeQ dstTypeQ srcName dstName srcCon srcFields dstCon dstFields = do
  inst <- resolveFieldRulesAndBuildInstanceWithType config srcTypeQ srcName srcCon srcFields dstTypeQ dstName dstCon dstFields
  pure [inst]

deriveMultiConstructor :: Config -> Name -> Name -> [ConstructorInfo] -> [ConstructorInfo] -> Q [Dec]
deriveMultiConstructor config@AutoMapConfig{..} srcName dstName srcCons dstCons = do
  sVar <- newName "s"

  matches <- case (srcCons, dstCons) of
    ([srcCon], [dstCon]) -> do
      m <- deriveConstructorMatch config srcName dstName srcCon dstCon
      return [m]

    _ -> forM srcCons $ \srcCon -> do
      let srcConName = getConstructorName srcCon
          srcTransformed = applyConstructorMapping constructorMap (nameBase srcConName)
          maybeDstCon = find (\c -> map toLower (nameBase (getConstructorName c)) == map toLower srcTransformed) dstCons
      case maybeDstCon of
        Just dstCon -> deriveConstructorMatch config srcName dstName srcCon dstCon
        Nothing -> fail $ "Witch.TH: no matching destination constructor for " <> show srcConName

  let body = caseE (varE sVar) (map pure matches)

  instanceD (pure []) [t| W.From $(conT srcName) $(conT dstName) |]
    [ funD 'W.from [clause [varP sVar] (normalB body) []] ] >>= pure . (:[])

deriveMultiConstructorWithType :: Config -> Q Type -> Q Type -> Name -> Name -> [ConstructorInfo] -> [ConstructorInfo] -> Q [Dec]
deriveMultiConstructorWithType config@AutoMapConfig{..} srcTypeQ dstTypeQ srcName dstName srcCons dstCons = do
  sVar <- newName "s"

  matches <- case (srcCons, dstCons) of
    ([srcCon], [dstCon]) -> do
      m <- deriveConstructorMatch config srcName dstName srcCon dstCon
      return [m]

    _ -> forM srcCons $ \srcCon -> do
      let srcConName = getConstructorName srcCon
          srcTransformed = applyConstructorMapping constructorMap (nameBase srcConName)
          maybeDstCon = find (\c -> map toLower (nameBase (getConstructorName c)) == map toLower srcTransformed) dstCons
      case maybeDstCon of
        Just dstCon -> deriveConstructorMatch config srcName dstName srcCon dstCon
        Nothing -> fail $ "Witch.TH: no matching destination constructor for " <> show srcConName

  let body = caseE (varE sVar) (map pure matches)

  instanceD (pure []) [t| W.From $srcTypeQ $dstTypeQ |]
    [ funD 'W.from [clause [varP sVar] (normalB body) []] ] >>= pure . (:[])

deriveConstructorMatch :: Config -> Name -> Name -> ConstructorInfo -> ConstructorInfo -> Q Match
deriveConstructorMatch config srcName dstName srcCon dstCon =
  case (srcCon, dstCon) of
    (RecordConstructor srcConName srcFields, RecordConstructor dstConName dstFields) ->
      deriveRecordMatch config srcName dstName srcConName srcFields dstConName dstFields

    (PositionalConstructor srcConName srcFields, PositionalConstructor dstConName dstFields) ->
      derivePositionalMatch srcConName srcFields dstConName dstFields

    _ -> fail "Witch.TH: cannot mix record and positional constructors"

derivePositionalMatch :: Name -> [(Bang, Type)] -> Name -> [(Bang, Type)] -> Q Match
derivePositionalMatch srcConName srcFields dstConName dstFields = do
  when (length srcFields /= length dstFields) $
    fail $ "Witch.TH: arity mismatch in " <> show srcConName

  srcVars <- mapM (\i -> newName ("x" ++ show i)) [1..length srcFields]
  let pat = ConP srcConName [] (map VarP srcVars)
      bodyExp = foldl AppE (ConE dstConName) [AppE (VarE 'W.into) (VarE v) | v <- srcVars]

  match (pure pat) (normalB (pure bodyExp)) []

deriveRecordMatch :: Config -> Name -> Name -> Name -> [(Name, Bang, Type)] -> Name -> [(Name, Bang, Type)] -> Q Match
deriveRecordMatch config srcName dstName srcCon srcFields dstCon dstFields =
  resolveFieldRulesForMatch config srcName srcCon srcFields dstName dstCon dstFields