@startuml
skinparam style strictuml
skinparam defaultFontName "DM Sans"
skinparam defaultFontSize 14
skinparam titleFontName "Volkhov"
skinparam titleFontSize 20
skinparam backgroundColor #FAFCF9
skinparam defaultFontColor #28342A
skinparam titleFontColor #28342A
skinparam ArrowColor #628A5B

skinparam class {
  BackgroundColor #FAFCF9
  HeaderBackgroundColor #EAF0E8
  BorderColor #547461
  ArrowColor #628A5B
  FontColor #28342A
}

skinparam note {
  BackgroundColor #EAF0E8
  BorderColor #547461
  FontColor #28342A
}

title The Biergarten Data Pipeline - Class Diagram

class BiergartenPipelineOrchestrator {
  - logger_ : std::shared_ptr<ILogger>
  - context_service_ : std::unique_ptr<IEnrichmentService>
  - generator_ : std::unique_ptr<DataGenerator>
  - exporter_ : std::unique_ptr<IExportService>
  - curated_data_service_ : std::unique_ptr<ICuratedDataService>
  - application_options_ : ApplicationOptions
  - generated_breweries_ : std::vector<BreweryRecord>
  - generated_users_ : std::vector<UserRecord>
  + Run() : bool
  - QueryCitiesWithCountries() : std::vector<Location>
  - GenerateBreweries(cities : std::span<const EnrichedCity>) : void
  - GenerateUsers(cities : std::span<const EnrichedCity>) : void
  - LogResults() : void
}

class LogLevel <<enumeration>> {
  Debug
  Info
  Warn
  Error
}

class PipelinePhase <<enumeration>> {
  Startup
  UserGeneration
  BreweryAndBeerGeneration
  CheckinGeneration
  RatingGeneration
  FollowGeneration
  Teardown
}

struct LogDTO {
  + level : LogLevel
  + phase : PipelinePhase
  + message : std::string
}

struct LogEntry {
  + timestamp : std::chrono::system_clock::time_point
  + origin : std::source_location
  + thread_id : std::thread::id
  + level : LogLevel
  + phase : PipelinePhase
  + message : std::string
}

interface ILogger <<interface>> {
  + Log(payload : LogDTO) : void
  - {abstract} DoLog(entry : LogEntry) : void
}

class LogProducer {
  - channel_ : BoundedChannel<LogEntry>&
  - DoLog(entry : LogEntry) : void
}

class LogDispatcher {
  - channel_ : BoundedChannel<LogEntry>&
  + Run() : void
  - ToSpdlogLevel(level) : spdlog::level::level_enum
}

interface IEnrichmentService <<interface>> {
  + GetLocationContext(loc : const Location&) : std::string
}

class MockEnrichmentService {
  + GetLocationContext(loc : const Location&) : std::string
}

class WikipediaEnrichmentService {
  - client_ : std::unique_ptr<WebClient>
  - extract_cache_ : std::unordered_map<std::string, std::string>
  + GetLocationContext(loc : const Location&) : std::string
  - FetchExtract(query : std::string_view) : std::string
}

interface WebClient <<interface>> {
  + Get(url : const std::string&) : std::string
  + UrlEncode(value : const std::string&) : std::string
}

class HttpWebClient {
  + Get(url : const std::string&) : std::string
  + UrlEncode(value : const std::string&) : std::string
}

interface DataGenerator <<interface>> {
  + GenerateBrewery(location : const Location&, region_context : const std::string&) : BreweryResult
  + GenerateUser(city : const EnrichedCity&, persona : const UserPersona&, name : const Name&) : UserResult
}

class MockGenerator {
  + GenerateBrewery(location : const Location&, region_context : const std::string&) : BreweryResult
  + GenerateUser(city : const EnrichedCity&, persona : const UserPersona&, name : const Name&) : UserResult
  - DeterministicHash(location : const Location&) : size_t
  - DeterministicHash(location : const Location&, persona : const UserPersona&, name : const Name&) : size_t
}

class LlamaGenerator {
  - model_ : ModelHandle
  - context_ : ContextHandle
  - prompt_formatter_ : std::unique_ptr<IPromptFormatter>
  - prompt_directory_ : std::unique_ptr<IPromptDirectory>
  - rng_ : std::mt19937
  + GenerateBrewery(...) : BreweryResult
  + GenerateUser(...) : UserResult
  - Load(model_path : const std::string&) : void
  - Infer(...) : std::string
  - InferFormatted(...) : std::string
}

interface IPromptFormatter <<interface>> {
  + Format(system_prompt : std::string_view, user_prompt : std::string_view) : std::string
}

class Gemma4JinjaPromptFormatter {
  + Format(system_prompt : std::string_view, user_prompt : std::string_view) : std::string
}

interface IPromptDirectory <<interface>> {
  + Load(key : std::string_view) : std::string
}

class PromptDirectory {
  - prompt_dir_ : std::filesystem::path
  - cache_ : std::unordered_map<std::string, std::string>
  + Load(key : std::string_view) : std::string
}

interface ICuratedDataService <<interface>> {
  + LoadLocations(filepath : const std::filesystem::path&) : const std::vector<Location>&
  + LoadPersonas(filepath : const std::filesystem::path&) : const std::vector<UserPersona>&
  + LoadForenamesByCountry(filepath : const std::filesystem::path&) : const std::unordered_map<std::string, forename_list>&
  + LoadSurnamesByCountry(filepath : const std::filesystem::path&) : const std::unordered_map<std::string, surname_list>&
}

class JsonLoader {
  - cache_ : cache
  + LoadLocations(filepath : const std::filesystem::path&) : const std::vector<Location>&
  + LoadPersonas(filepath : const std::filesystem::path&) : const std::vector<UserPersona>&
  + LoadForenamesByCountry(filepath : const std::filesystem::path&) : const std::unordered_map<std::string, forename_list>&
  + LoadSurnamesByCountry(filepath : const std::filesystem::path&) : const std::unordered_map<std::string, surname_list>&
}

note right of JsonLoader
  Each Load* method memoizes its result in
  cache_ on first call; later calls on the
  same instance return the cached value
  without re-parsing.
end note

class MockCuratedDataService {
  - locations_ : std::vector<Location>
  - personas_ : std::vector<UserPersona>
  - forenames_by_country_ : std::unordered_map<std::string, forename_list>
  - surnames_by_country_ : std::unordered_map<std::string, surname_list>
  + LoadLocations(filepath : const std::filesystem::path&) : const std::vector<Location>&
  + LoadPersonas(filepath : const std::filesystem::path&) : const std::vector<UserPersona>&
  + LoadForenamesByCountry(filepath : const std::filesystem::path&) : const std::unordered_map<std::string, forename_list>&
  + LoadSurnamesByCountry(filepath : const std::filesystem::path&) : const std::unordered_map<std::string, surname_list>&
}

note right of MockCuratedDataService
  Fixed 4-location / 3-persona dataset
  (US, DE, FR, BE) used in --mocked runs;
  filepath arguments are ignored.
end note

interface IExportService <<interface>> {
  + Initialize() : void
  + ProcessRecord(brewery : const BreweryRecord&) : uint64_t
  + ProcessRecord(user : const UserRecord&) : uint64_t
  + Finalize() : void
}

class SqliteExportService {
  - date_time_provider_ : std::unique_ptr<IDateTimeProvider>
  - run_timestamp_utc_ : std::string
  - database_path_ : std::filesystem::path
  - db_handle_ : SqliteDatabaseHandle
  - insert_location_stmt_ : SqliteStatementHandle
  - insert_brewery_stmt_ : SqliteStatementHandle
  - insert_user_stmt_ : SqliteStatementHandle
  - transaction_open_ : bool
  - location_cache_ : std::unordered_map<std::string, sqlite3_int64>
  + Initialize() : void
  + ProcessRecord(brewery : const BreweryRecord&) : uint64_t
  + ProcessRecord(user : const UserRecord&) : uint64_t
  + Finalize() : void
  - InitializeSchema() : void
  - ResolveLocationId(location : const Location&) : sqlite3_int64
}

interface IDateTimeProvider <<interface>> {
  + GetUtcTimestamp() : std::string
}

class SystemDateTimeProvider {
  + GetUtcTimestamp() : std::string
}

' Structural Relationships / Dependency Injection
BiergartenPipelineOrchestrator *-- ILogger : owns
BiergartenPipelineOrchestrator *-- IEnrichmentService : owns
BiergartenPipelineOrchestrator *-- DataGenerator : owns
BiergartenPipelineOrchestrator *-- IExportService : owns
BiergartenPipelineOrchestrator *-- ICuratedDataService : owns

LogEntry *-- LogLevel
LogEntry *-- PipelinePhase
LogDTO *-- LogLevel
LogDTO *-- PipelinePhase
ILogger <|.. LogProducer : implements
LogProducer ..> LogEntry : emits
LogDispatcher ..> LogEntry : consumes

IEnrichmentService <|.. WikipediaEnrichmentService : implements
IEnrichmentService <|.. MockEnrichmentService : implements
WikipediaEnrichmentService *-- WebClient : owns

WebClient <|.. HttpWebClient : implements

DataGenerator <|.. MockGenerator : implements
DataGenerator <|.. LlamaGenerator : implements

LlamaGenerator *-- IPromptFormatter : uses
LlamaGenerator *-- IPromptDirectory : uses

IPromptFormatter <|.. Gemma4JinjaPromptFormatter : implements
IPromptDirectory <|.. PromptDirectory : implements

ICuratedDataService <|.. JsonLoader : implements
ICuratedDataService <|.. MockCuratedDataService : implements

IExportService <|.. SqliteExportService : implements
SqliteExportService *-- IDateTimeProvider : owns
IDateTimeProvider <|.. SystemDateTimeProvider : implements

@enduml
