' ============================================================
' WALKGUIDE — DESIGN PATTERNS (GoF)
' Flutter × Spring Boot × In-Device AI
' 7 Patterns: 2 Creational · 2 Structural · 3 Behavioral
' ============================================================

@startuml WalkGuide_Design_Patterns

skinparam monochrome false
skinparam shadowing false
skinparam defaultFontName Arial
skinparam defaultFontSize 12
skinparam roundCorner 10
skinparam ArrowColor #555555
skinparam ArrowThickness 1.2

skinparam class {
    BackgroundColor #FAFAFA
    BorderColor #AAAAAA
    HeaderBackgroundColor #E8E8E8
    FontColor #222222
    StereotypeFontColor #666666
    AttributeFontColor #333333
}

skinparam package {
    BackgroundColor #F5F5F5
    BorderColor #888888
    FontColor #333333
    FontStyle bold
}

skinparam note {
    BackgroundColor #FFFDE7
    BorderColor #F9A825
    FontColor #444444
    FontSize 11
}

' ============================================================
' PATTERN 1 — BUILDER (Creational)
' ============================================================

package "①  Builder Pattern  [Creational]" #EEF6EE {

    interface "UserBuilder\n<<Builder>>" as UserBuilder {
        + email(String) : UserBuilder
        + password(String) : UserBuilder
        + displayName(String) : UserBuilder
        + uniqueUserId(String) : UserBuilder
        + fcmToken(String) : UserBuilder
        + role(Role) : UserBuilder
        + build() : User
    }

    class "User\n<<Entity>>" as User {
        - id : Long
        - email : String
        - password : String
        - displayName : String
        - uniqueUserId : String
        - fcmToken : String
        - role : Role
        - createdAt : Timestamp
        + {static} builder() : UserBuilder
    }

    class "FcmService\n<<Service>>" as FcmService {
        + sendPushNotification(userId, payload)
        + sendSosAlert(guardianId, lat, lng)
        - buildMessage(token, title, body) : Message
    }

    class "AuthService\n<<Service>>" as AuthService {
        + register(RegisterRequest) : AuthDataResponse
        + login(LoginRequest) : AuthDataResponse
        - buildAuthResponse(user, tokens) : AuthDataResponse
    }

    UserBuilder ..> User : <<creates>>
    User ..> UserBuilder : <<returns>>
    FcmService ..> "Firebase\nMessage.builder()" : uses
    AuthService ..> "AuthDataResponse\n.builder()" : uses
}

note bottom of UserBuilder
  Lombok @Builder pada User.java
  Memisahkan konstruksi objek kompleks
  dari representasinya. Optional fields
  (displayName, uniqueUserId, fcmToken)
  tidak perlu constructor overloading.
end note

' ============================================================
' PATTERN 2 — SINGLETON (Creational)
' ============================================================

package "②  Singleton Pattern  [Creational]" #EEF0FF {

    class "GetIt\n<<ServiceLocator>>" as GetIt {
        - {static} _instance : GetIt
        + {static} instance : GetIt
        + registerSingleton<T>(T)
        + get<T>() : T
    }

    class "TtsService\n<<Singleton>>" as TtsService {
        - {static} _instance : TtsService
        - _flutterTts : FlutterTts
        + speak(String text)
        + speakImmediate(String text)
        + stop()
        + setLanguage(String lang)
    }

    class "SttService\n<<Singleton>>" as SttService {
        - {static} _instance : SttService
        - _speechToText : SpeechToText
        + startListening()
        + stopListening()
        + onResult : Stream<String>
    }

    class "YoloDetector\n<<Singleton>>" as YoloDetector {
        - {static} _instance : YoloDetector
        - _interpreter : Interpreter
        - _labels : List<String>
        + loadModel()
        + detect(CameraImage) : List<DetectionResult>
        + isRunning : bool
    }

    class "WebSocketService\n<<Singleton>>" as WebSocketService {
        - {static} _instance : WebSocketService
        - _stompClient : StompClient
        + connect(String token)
        + subscribe(String destination)
        + disconnect()
    }

    class "AgoraService\n<<Singleton>>" as AgoraService {
        - {static} _instance : AgoraService
        - _engine : RtcEngine
        + joinChannel(token, channel, uid)
        + leaveChannel()
        + muteLocalAudio(bool)
    }

    GetIt --> TtsService : registers &\nmanages
    GetIt --> SttService : registers &\nmanages
    GetIt --> YoloDetector : registers &\nmanages
    GetIt --> WebSocketService : registers &\nmanages
    GetIt --> AgoraService : registers &\nmanages
}

note right of GetIt
  injection_container.dart:
  sl.registerSingleton<TtsService>(TtsService())
  sl.registerSingleton<YoloDetector>(YoloDetector())
  sl.registerSingleton<WebSocketService>(WebSocketService())
  Satu instance untuk seluruh lifecycle app.
end note

' ============================================================
' PATTERN 3 — FACADE (Structural) — Flutter
' ============================================================

package "③  Facade Pattern  [Structural]" #FFF8E1 {

    class "VoiceCommandHandler\n<<Facade>>" as VoiceCommandHandler {
        - _ttsService : TtsService
        - _sttService : SttService
        - _router : GoRouter
        - _walkGuideBloc : WalkGuideBloc
        - _sosBloc : SosBloc
        - _notifBloc : NotificationBloc
        + processText(String command) : void
        - _matchCommand(String) : VoiceCommandKey?
        - _executeCommand(VoiceCommandKey) : void
    }

    class "WalkGuideBloc\n<<Client>>" as WalkGuideBlocFacade {
        + onVoiceCommand(String text)
    }

    class "GuardianDashboardService\n<<Facade>>" as GuardianDashboardService {
        - _locationService : LocationService
        - _activityService : ActivityLogService
        - _sosService : SosService
        - _notifService : NotificationService
        + getDashboard(guardianId) : DashboardResponse
    }

    class "SttService " as SttServiceFacade <<Subsystem>>
    class "TtsService " as TtsServiceFacade <<Subsystem>>
    class "GoRouter\n<<Router>>" as GoRouterFacade <<Subsystem>>
    class "SosBloc " as SosBlocFacade <<Subsystem>>

    class "LocationService\n<<Service>>" as LocationService <<Subsystem>>
    class "ActivityLogService\n<<Service>>" as ActivityService <<Subsystem>>
    class "SosService\n<<Service>>" as SosServiceFacade <<Subsystem>>

    WalkGuideBlocFacade --> VoiceCommandHandler : processText()
    VoiceCommandHandler --> SttServiceFacade : delegates
    VoiceCommandHandler --> TtsServiceFacade : delegates
    VoiceCommandHandler --> GoRouterFacade : delegates
    VoiceCommandHandler --> SosBlocFacade : delegates

    GuardianDashboardService --> LocationService : aggregates
    GuardianDashboardService --> ActivityService : aggregates
    GuardianDashboardService --> SosServiceFacade : aggregates
}

note right of VoiceCommandHandler
  Client hanya panggil processText("start walkguide")
  tanpa perlu tahu kompleksitas di baliknya:
  matching phrase → execute command → TTS feedback
  → route navigation → BLoC event dispatch
end note

' ============================================================
' PATTERN 4 — REPOSITORY / PROXY (Structural)
' ============================================================

package "④  Repository Pattern  [Structural — Proxy]" #FFF3E0 {

    interface "WalkGuideRepository\n<<interface>>" as WalkGuideRepo {
        + startSession() : Either<Failure, void>
        + stopSession() : Either<Failure, void>
        + logObstacle(ObstacleLogRequest) : Either<Failure, void>
        + getObstacleLogs() : Either<Failure, List<ObstacleLog>>
    }

    interface "ActivityLogRepository\n<<interface>>" as ActivityRepo {
        + getLogs(page) : Either<Failure, List<ActivityLog>>
        + savePending(ActivityLog) : Either<Failure, void>
        + syncPending() : Either<Failure, void>
    }

    class "WalkGuideRepositoryImpl\n<<Proxy>>" as WalkGuideRepoImpl {
        - _remoteDataSource : WalkGuideRemoteDataSource
        - _localDataSource : WalkGuideLocalDataSource
        - _connectivity : ConnectivityPlus
        + startSession() : Either<Failure, void>
        + logObstacle(req) : Either<Failure, void>
        - _isOnline() : bool
    }

    class "ActivityLogRepositoryImpl\n<<Proxy>>" as ActivityRepoImpl {
        - _remoteDataSource : ActivityRemoteDataSource
        - _localDataSource : ActivityLocalDataSource
        + getLogs(page) : Either<Failure, List>
        + syncPending() : Either<Failure, void>
    }

    class "WalkGuideRemoteDataSource\n<<Remote>>" as RemoteDSWalk {
        + startSession() : void
        + logObstacle(req) : void
        ' POST /api/v1/user/obstacle
    }

    class "WalkGuideLocalDataSource\n<<SQLite/Drift>>" as LocalDSWalk {
        + cacheObstacle(ObstacleLog) : void
        + getPendingLogs() : List<ObstacleLog>
        ' Drift ORM — offline first
    }

    WalkGuideRepo <|.. WalkGuideRepoImpl : implements
    ActivityRepo <|.. ActivityRepoImpl : implements
    WalkGuideRepoImpl --> RemoteDSWalk : online path
    WalkGuideRepoImpl --> LocalDSWalk : offline path
}

note bottom of WalkGuideRepoImpl
  Proxy memutuskan sumber data:
  if (isOnline) → fetch remote API
  else → baca/tulis SQLite (Drift)
  Domain layer tidak tahu perbedaannya.
  dartz Either<Failure, Data> untuk typed error.
end note

' ============================================================
' PATTERN 5 — OBSERVER (Behavioral)
' ============================================================

package "⑤  Observer Pattern  [Behavioral]" #F3E5F5 {

    abstract class "Bloc<Event, State>\n<<Subject>>" as BlocSubject {
        # stateController : StreamController<State>
        + {abstract} on<E>(EventHandler)
        + add(Event event)
        + emit(State state)
        + stream : Stream<State>
    }

    class "WalkGuideBloc\n<<ConcreteSubject>>" as WalkGuideBlocObs {
        + on<StartWalkGuide>(_onStart)
        + on<StopWalkGuide>(_onStop)
        + on<CameraFrameReceived>(_onFrame)
        + on<ObstacleDetected>(_onObstacle)
        - _yoloDetector : YoloDetector
        - _ttsService : TtsService
        - _hapticService : HapticService
    }

    class "BlocBuilder<WalkGuideBloc, WalkGuideState>\n<<Observer / Widget>>" as BlocBuilderWidget {
        + builder(ctx, state) : Widget
        ' Rebuilds UI on every state emission
    }

    class "BlocListener<WalkGuideBloc, WalkGuideState>\n<<Observer / SideEffect>>" as BlocListenerWidget {
        + listener(ctx, state) : void
        ' Side effects: TTS, haptic, navigation
    }

    class "WebSocketService\n<<Subject>>" as WebSocketObs {
        - _stompClient : StompClient
        + subscribe(destination) : Stream<dynamic>
        + onLocationUpdate : StreamController
        + onSosAlert : StreamController
        + onNotification : StreamController
    }

    class "GuardianMapScreen\n<<Observer>>" as GuardianMapObs {
        + onLocationUpdate(LocationData)
        ' Updates flutter_map markers in real-time
    }

    BlocSubject <|-- WalkGuideBlocObs : extends
    WalkGuideBlocObs --> BlocBuilderWidget : emits state\n(rebuilds UI)
    WalkGuideBlocObs --> BlocListenerWidget : emits state\n(side effects)
    WebSocketObs --> GuardianMapObs : notifies\nlive location
}

note left of WebSocketObs
  Guardian subscribe topic:
  /topic/location/{userId}  — live map
  /queue/sos/{guardianId}   — SOS alert
  /queue/notif/{userId}     — notifications
  STOMP over SockJS (stomp_dart_client)
end note

' ============================================================
' PATTERN 6 — STRATEGY (Behavioral)
' ============================================================

package "⑥  Strategy Pattern  [Behavioral]" #E8F5E9 {

    interface "ObstacleAlertStrategy\n<<Strategy>>" as AlertStrategy {
        + alert(DetectionResult result) : void
    }

    class "TtsOnlyStrategy\n<<ConcreteStrategy>>" as TtsOnly {
        - _ttsService : TtsService
        + alert(result) : void
        ' TTS: "Caution! {label} {direction}. {distance}."
    }

    class "TtsWithHapticStrategy\n<<ConcreteStrategy>>" as TtsHaptic {
        - _ttsService : TtsService
        - _hapticService : HapticService
        + alert(result) : void
        ' TTS + vibration pattern
    }

    class "HapticOnlyStrategy\n<<ConcreteStrategy>>" as HapticOnly {
        - _hapticService : HapticService
        + alert(result) : void
        ' Vibration only (silent environment)
    }

    class "ObstacleAnalyzer\n<<Context>>" as ObstacleAnalyzerCtx {
        - _strategy : ObstacleAlertStrategy
        - _aiConfig : AiConfig
        + setStrategy(ObstacleAlertStrategy)
        + analyze(List<DetectionResult>) : void
        + prioritize(results) : DetectionResult
        + calculateDirection(bbox) : Direction
    }

    class "AiConfig\n<<Config>>" as AiConfigStrategy {
        + alertMode : AlertMode
        + confidenceThreshold : double
        + maxInferenceFps : int
        + enabledLabels : List<String>
    }

    AlertStrategy <|.. TtsOnly : implements
    AlertStrategy <|.. TtsHaptic : implements
    AlertStrategy <|.. HapticOnly : implements
    ObstacleAnalyzerCtx --> AlertStrategy : uses
    ObstacleAnalyzerCtx ..> AiConfigStrategy : reads alertMode\nto select strategy
}

note right of ObstacleAnalyzerCtx
  Guardian dapat ganti strategy dari dashboard:
  alertMode = TTS_ONLY | TTS_HAPTIC | HAPTIC_ONLY
  PUT /api/v1/guardian/ai-config
  Strategy dipilih runtime tanpa ubah kode utama.
end note

' ============================================================
' PATTERN 7 — CHAIN OF RESPONSIBILITY (Behavioral)
' ============================================================

package "⑦  Chain of Responsibility  [Behavioral]" #FCE4EC {

    ' --- Flutter: Dio Interceptor Chain ---
    abstract class "Interceptor\n<<Handler>>" as DioInterceptor {
        + onRequest(options, handler)
        + onResponse(response, handler)
        + onError(error, handler)
    }

    class "AuthInterceptor\n<<ConcreteHandler>>" as AuthInterceptor {
        - _secureStorage : FlutterSecureStorage
        + onRequest(options, handler)
        ' Attach Bearer token ke setiap request
        + onError(error, handler)
        ' Jika 401: refresh token → retry request
    }

    class "ErrorInterceptor\n<<ConcreteHandler>>" as ErrorInterceptor {
        + onError(error, handler)
        ' Map HTTP error codes → Failure domain objects
        ' 401 → UnauthorizedFailure
        ' 404 → NotFoundFailure
        ' 500 → ServerFailure
    }

    class "LogInterceptor\n<<ConcreteHandler>>" as LogInterceptorFlutter {
        + onRequest(options, handler)
        + onResponse(response, handler)
        + onError(error, handler)
        ' Print request/response untuk debugging
    }

    class "ApiClient\n<<Dio>>" as ApiClient {
        - _dio : Dio
        + get(url) : Response
        + post(url, data) : Response
        + put(url, data) : Response
        ' Interceptors dieksekusi berurutan
    }

    ' --- Backend: Spring Security Filter Chain ---
    abstract class "OncePerRequestFilter\n<<Handler>>" as SpringFilter {
        + {abstract} doFilterInternal(req, res, chain)
        + doFilter(req, res, chain)
    }

    class "JwtAuthFilter\n<<ConcreteHandler>>" as JwtAuthFilter {
        - _jwtUtil : JwtUtil
        - _userDetailsService : UserDetailsService
        + doFilterInternal(req, res, chain)
        ' Extract JWT → validate → set SecurityContext
        ' Jika invalid: 401 Unauthorized langsung
    }

    class "SecurityConfig\n<<Config/Handler>>" as SecurityConfigChain {
        + securityFilterChain(http) : SecurityFilterChain
        ' /auth/** → permitAll
        ' /api/v1/** → authenticated
        ' ROLE_GUARDIAN vs ROLE_USER routing
    }

    class "Controller\n<<Endpoint>>" as ControllerChain {
        + handleRequest(request) : ResponseEntity
        ' Request tiba hanya jika lolos semua filter
    }

    DioInterceptor <|-- AuthInterceptor : extends
    DioInterceptor <|-- ErrorInterceptor : extends
    DioInterceptor <|-- LogInterceptorFlutter : extends

    ApiClient --> AuthInterceptor : chain[0]
    AuthInterceptor --> ErrorInterceptor : passes to\nchain[1]
    ErrorInterceptor --> LogInterceptorFlutter : passes to\nchain[2]

    SpringFilter <|-- JwtAuthFilter : extends
    JwtAuthFilter --> SecurityConfigChain : passes to
    SecurityConfigChain --> ControllerChain : passes to\n(if authorized)
}

note bottom of ApiClient
  Flutter — Dio interceptor chain:
  HTTP Request → AuthInterceptor → ErrorInterceptor → LogInterceptor → Response
  Setiap handler bisa: proses, modifikasi, atau blok request.
  AuthInterceptor otomatis refresh JWT jika expired (401).
end note

note bottom of JwtAuthFilter
  Backend — Spring Security filter chain:
  HTTP Request → JwtAuthFilter → SecurityConfig → Controller
  JwtAuthFilter extract & validasi Bearer token setiap request.
  Jika invalid → langsung 401 tanpa lanjut ke controller.
end note

@enduml
