kasl-cli 1.0.0

kasl is a comprehensive command-line utility 🛠️ designed to streamline the tracking of work activities 📊, including start times ⏰, pauses ⏸, and task completion
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
//! Internal SiServer API client for company reporting and calendar integration.
//!
//! Provides integration with an internal company API system that handles employee
//! time tracking reports and company calendar information.
//!
//! ## Features
//!
//! - **Report Submission**: Submit daily and monthly time tracking reports
//! - **Calendar Integration**: Fetch company rest dates and holidays
//! - **Two-Stage Authentication**: LDAP authentication followed by session token exchange
//! - **Error Resilience**: Graceful handling of network failures and API errors
//! - **Session Management**: Automatic session caching and renewal
//!
//! ## Usage
//!
//! ```rust,no_run
//! # use kasl::api::si::{Si, SiConfig};
//! # use chrono::Local;
//! # async fn f() -> anyhow::Result<()> {
//! let config = SiConfig {
//!     login: "username".to_string(),
//!     auth_url: "https://auth.company.com".to_string(),
//!     api_url: "https://api.company.com".to_string(),
//! };
//!
//! let mut si = Si::new(&config);
//! let today = Local::now().date_naive();
//! let rest_dates = si.rest_dates(today).await?;
//! # Ok(())
//! # }
//! ```

use crate::{
    api::Session,
    libs::{config::ConfigModule, messages::Message, secret::Secret},
    msg_error, msg_print,
};
use anyhow::Result;
use base64::prelude::*;
use chrono::{Datelike, Duration, NaiveDate, Weekday};
use dialoguer::{Input, theme::ColorfulTheme};
use reqwest::{
    Client, StatusCode,
    header::{self, COOKIE, HeaderMap, HeaderValue},
    multipart,
};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;

/// Maximum number of authentication retries before giving up.
/// SiServer has more complex auth flow, so we use the same conservative limit.
const MAX_RETRY_COUNT: i32 = 3;

/// Cookie name prefix used by SiServer for session identification.
const COOKIE_KEY: &str = "PORTALSESSID=";

/// Filename for storing SiServer session tokens in the user data directory.
const SESSION_ID_FILE: &str = ".si_session_id";

/// Filename for storing encrypted SiServer credentials for password caching.
const SECRET_FILE: &str = ".si_secret";

/// SiServer API endpoint for LDAP authentication (first stage).
const AUTH_URL: &str = "auth/ldap";

/// SiServer API endpoint for token-to-session exchange (second stage).
const LOGIN_URL: &str = "auth/login-by-token";

/// SiServer API endpoint for submitting daily time reports.
const REPORT_URL: &str = "report-card/send-daily-report";

/// SiServer API endpoint for submitting monthly summary reports.
const MONTHLY_REPORT_URL: &str = "report-card/send-monthly-report";

/// SiServer API endpoint for fetching company rest dates and holidays.
const REST_DATES_URL: &str = "report-card/get-rest-dates";

/// User credentials for SiServer authentication.
///
/// SiServer requires special password encoding (double base64) for security.
/// Credentials are only held in memory during the authentication process.
#[derive(Serialize, Clone, Debug)]
pub struct LoginCredentials {
    /// Username for LDAP authentication
    login: String,
    /// Double base64-encoded password for enhanced security
    password: String,
}

/// Response structure for SiServer LDAP authentication.
///
/// The first stage of authentication returns a temporary token that must
/// be exchanged for a session cookie in the second stage.
#[derive(Deserialize)]
pub struct AuthSession {
    /// Payload containing the authentication token
    payload: AuthPayload,
}

/// Authentication payload containing the temporary token.
///
/// This token is used to authenticate the second stage of the login process
/// where it's exchanged for a session cookie.
#[derive(Deserialize)]
pub struct AuthPayload {
    /// Temporary authentication token for session exchange
    token: String,
}

/// Response structure for SiServer rest dates API.
///
/// SiServer provides company calendar information including various types
/// of non-working days such as holidays, vacation days, and weekend days.
/// Different date arrays represent different types of rest periods.
#[derive(Debug, Deserialize)]
pub struct RestDatesResponse {
    /// Regular rest dates (general holidays)
    dates: Vec<String>,
    /// Vacation dates (company-specific holidays)
    v_dates: Vec<String>,
    /// Weekend dates (extended weekend periods)
    w_dates: Vec<String>,
}

impl RestDatesResponse {
    /// Parses and combines all rest dates into a single unified set.
    ///
    /// This method processes all three categories of rest dates and combines them
    /// into a single `HashSet` for easy lookup operations. Duplicate dates across
    /// categories are automatically deduplicated.
    ///
    /// ## Date Format Handling
    ///
    /// The API returns dates in "YYYY-MM-DD" format. Invalid date strings are
    /// silently ignored to handle potential API inconsistencies gracefully.
    ///
    /// # Returns
    ///
    /// * `Result<HashSet<NaiveDate>>` - Unified set of all rest dates
    ///
    /// # Errors
    ///
    /// Currently cannot fail, but returns `Result` for future error handling
    /// such as date validation or API response verification.
    pub fn unique_dates(&self) -> Result<HashSet<NaiveDate>> {
        let mut date_set = HashSet::new();

        // Process all three date categories
        self.process_dates(&self.dates, &mut date_set)?;
        self.process_dates(&self.v_dates, &mut date_set)?;
        self.process_dates(&self.w_dates, &mut date_set)?;

        Ok(date_set)
    }

    /// Helper function to parse date strings and add them to the result set.
    ///
    /// Processes a vector of date strings, attempting to parse each one into
    /// a `NaiveDate`. Invalid dates are silently skipped to handle API
    /// inconsistencies without failing the entire operation.
    ///
    /// # Arguments
    ///
    /// * `dates` - Vector of date strings in "YYYY-MM-DD" format
    /// * `date_set` - Mutable reference to the result set for adding parsed dates
    ///
    /// # Returns
    ///
    /// Always returns `Ok(())` as this operation cannot fail.
    fn process_dates(&self, dates: &[String], date_set: &mut HashSet<NaiveDate>) -> Result<()> {
        dates
            .iter()
            .filter_map(|date_str| NaiveDate::parse_from_str(date_str, "%Y-%m-%d").ok())
            .for_each(|date| {
                date_set.insert(date);
            });
        Ok(())
    }
}

/// SiServer API client with advanced session management.
///
/// This client handles the complex two-stage authentication flow required by
/// SiServer and provides methods for report submission and calendar data retrieval.
/// It implements resilient error handling to ensure application stability.
///
/// ## Thread Safety
///
/// The client is not thread-safe due to mutable retry state. Each thread
/// should use its own client instance for concurrent operations.
///
/// ## Authentication Architecture
///
/// SiServer uses a sophisticated authentication system:
/// 1. **LDAP Stage**: Credentials sent to LDAP endpoint, token received
/// 2. **Session Stage**: Token sent to session endpoint, cookie received
/// 3. **API Usage**: Cookie included in all subsequent API requests
///
/// This design provides enhanced security but requires careful session management.
#[derive(Debug)]
pub struct Si {
    /// HTTP client for making API requests with connection pooling
    client: Client,
    /// Configuration containing API endpoints and user information
    config: SiConfig,
    /// In-memory storage for authentication credentials during auth process
    credentials: Option<LoginCredentials>,
    /// Counter for tracking authentication retry attempts
    retries: i32,
}

impl Session for Si {
    /// Performs two-stage authentication with SiServer.
    ///
    /// This method implements SiServer's unique authentication flow which requires
    /// two separate API calls to establish a session. The process is more complex
    /// than standard session authentication but provides enhanced security.
    ///
    /// ## Authentication Process
    ///
    /// 1. **LDAP Authentication**: Send credentials to LDAP endpoint
    /// 2. **Token Extraction**: Parse authentication token from response
    /// 3. **Session Exchange**: Send token to session endpoint with Bearer auth
    /// 4. **Cookie Extraction**: Parse session cookie from Set-Cookie header
    /// 5. **Format Preparation**: Extract session ID for use in subsequent requests
    ///
    /// ## Error Scenarios
    ///
    /// - LDAP authentication failure (invalid credentials)
    /// - Token parsing failure (unexpected response format)
    /// - Session exchange failure (token expired or invalid)
    /// - Cookie extraction failure (missing or malformed Set-Cookie header)
    ///
    /// # Returns
    ///
    /// Returns the session ID string extracted from the PORTALSESSID cookie.
    ///
    /// # Errors
    ///
    /// Returns an error if:
    /// - No credentials have been set (programming error)
    /// - Network requests fail
    /// - LDAP authentication fails
    /// - Token or cookie parsing fails
    /// - Authentication flow completes but no valid session is established
    async fn login(&self) -> Result<String> {
        // Ensure credentials are available for authentication
        let credentials = self.credentials.clone().expect("Credentials not set!");

        // Stage 1: LDAP Authentication
        let auth_url = format!("{}/{}", self.config.auth_url, AUTH_URL);
        let auth_res = self.client.post(auth_url).json(&credentials).send().await?;
        let auth_body = auth_res.text().await?;
        let auth_session: AuthSession = serde_json::from_str(&auth_body)?;

        // Stage 2: Token-to-Session Exchange
        let login_url = format!("{}/{}", self.config.api_url, LOGIN_URL);
        let login_res = self
            .client
            .post(login_url)
            .header(header::AUTHORIZATION, format!("Bearer {}", auth_session.payload.token))
            .send()
            .await?;

        // Stage 3: Cookie Extraction
        if let Some(cookie) = login_res.headers().get("Set-Cookie")
            && let Ok(cookie_val) = cookie.to_str()
        {
            // Find the PORTALSESSID cookie in the Set-Cookie header
            if let Some(portalsessid) = cookie_val.split(";").find(|c| c.starts_with(COOKIE_KEY)) {
                let session_id = portalsessid.trim_start_matches(COOKIE_KEY);
                return Ok(session_id.to_string());
            }
        }

        // Authentication completed but no valid session cookie was found
        anyhow::bail!("Login failed")
    }

    /// Sets user credentials with SiServer-specific password encoding.
    ///
    /// SiServer requires passwords to be double base64-encoded for security.
    /// This method handles the encoding and stores credentials in memory for
    /// use during the authentication process.
    ///
    /// ## Password Encoding
    ///
    /// The password undergoes double base64 encoding:
    /// 1. First encoding: `base64(password)`
    /// 2. Second encoding: `base64(base64(password))`
    ///
    /// This provides additional security layers for credential transmission.
    ///
    /// # Arguments
    ///
    /// * `password` - The user's SiServer password in plain text
    ///
    /// # Returns
    ///
    /// Always returns `Ok(())` as encoding cannot fail.
    fn set_credentials(&mut self, password: &str) -> Result<()> {
        // Apply double base64 encoding as required by SiServer
        let encoded_password = BASE64_STANDARD.encode(BASE64_STANDARD.encode(password));

        self.credentials = Some(LoginCredentials {
            login: self.config.login.to_string(),
            password: encoded_password,
        });
        Ok(())
    }

    /// Returns the filename for storing SiServer session tokens.
    ///
    /// The session file is stored in the user's application data directory
    /// and contains the cached session token for automatic login restoration.
    fn session_id_file(&self) -> &str {
        SESSION_ID_FILE
    }

    /// Returns a configured Secret instance for secure password prompting.
    ///
    /// The Secret manager handles secure password input with hidden characters
    /// and optional encrypted caching in the user's data directory.
    ///
    /// # Returns
    ///
    /// A configured `Secret` instance with SiServer-specific prompts and file names.
    fn secret(&self) -> Secret {
        Secret::new(SECRET_FILE, "Enter your SiServer password")
    }

    /// Returns the current authentication retry count.
    ///
    /// Used by the session management system to track failed authentication
    /// attempts and implement retry limits.
    fn retry(&self) -> i32 {
        self.retries
    }

    /// Increments the authentication retry counter.
    ///
    /// Called after each failed authentication attempt to track progress
    /// toward the maximum retry limit.
    fn inc_retry(&mut self) {
        self.retries += 1;
    }

    /// Resets the authentication retry counter to zero.
    ///
    /// Called after successful authentication to ensure future session
    /// requests start with a clean slate.
    fn reset_retry(&mut self) {
        self.retries = 0;
    }
}

impl Si {
    /// Creates a new SiServer API client instance.
    ///
    /// Initializes the HTTP client with default settings suitable for SiServer API
    /// interactions. The client is configured for both JSON and multipart requests
    /// to handle different SiServer endpoints appropriately.
    ///
    /// # Arguments
    ///
    /// * `config` - SiServer configuration containing API endpoints and user information
    ///
    /// # Examples
    ///
    /// ```rust,no_run
    /// use kasl::api::si::{Si, SiConfig};
    ///
    /// let config = SiConfig {
    ///     login: "username".to_string(),
    ///     auth_url: "https://auth.company.com".to_string(),
    ///     api_url: "https://api.company.com".to_string(),
    /// };
    /// let si = Si::new(&config);
    /// ```
    pub fn new(config: &SiConfig) -> Self {
        Self {
            client: Client::new(),
            config: config.clone(),
            credentials: None,
            retries: 0,
        }
    }

    /// Submits a daily time tracking report to SiServer.
    ///
    /// This method sends formatted daily report data to the SiServer API for
    /// payroll and time tracking integration. It handles session management
    /// and implements retry logic for authentication failures.
    ///
    /// ## Report Format
    ///
    /// The report data should be a JSON string containing:
    /// - Work hours and break information
    /// - Task completion details
    /// - Productivity metrics
    /// - Any relevant metadata for the specified date
    ///
    /// ## Session Management
    ///
    /// The method implements automatic session handling:
    /// 1. **Session Retrieval**: Get or create a valid session token
    /// 2. **Report Submission**: Send report data with session authentication
    /// 3. **Error Handling**: Detect expired sessions and retry with re-authentication
    /// 4. **Status Return**: Return HTTP status for caller handling
    ///
    /// # Arguments
    ///
    /// * `data` - JSON string containing the formatted report data
    /// * `date` - The date for which the report is being submitted
    ///
    /// # Returns
    ///
    /// Returns the HTTP status code from the API response, allowing callers
    /// to determine success or specific failure modes.
    ///
    /// # Errors
    ///
    /// Returns an error if:
    /// - Session management fails persistently
    /// - Network request fails
    /// - Request formatting fails
    /// - Duration conversion fails (internal error)
    ///
    /// # Examples
    ///
    /// ```rust,no_run
    /// # use kasl::api::si::{Si, SiConfig};
    /// # use chrono::Local;
    /// # use anyhow::Result;
    /// # async fn example() -> Result<()> {
    /// # let config = SiConfig {
    /// #     login: "username".to_string(),
    /// #     auth_url: "https://auth.company.com".to_string(),
    /// #     api_url: "https://api.company.com".to_string(),
    /// # };
    /// let mut si = Si::new(&config);
    /// let report_data = r#"{"hours": 8, "tasks": 5}"#.to_string();
    /// let today = Local::now().date_naive();
    ///
    /// let status = si.send(&report_data, &today).await?;
    /// if status.is_success() {
    ///     println!("Report submitted successfully");
    /// }
    /// # Ok(())
    /// # }
    /// ```
    pub async fn send(&mut self, data: &str, date: &NaiveDate) -> Result<StatusCode> {
        let mut local_retries = 0;
        loop {
            // Get valid session for API request
            let session_id = self.get_session_id().await?;
            let url = format!("{}/{}", self.config.api_url, REPORT_URL);
            let date = date.format("%Y-%m-%d").to_string();

            // Prepare multipart form data for submission
            let form = multipart::Form::new()
                .text("date", date)
                .text("tasks", data.to_owned())
                .text("comment", "")
                .text("day_type", "1")
                .text("duty", "0")
                .text("only_save", "0");

            // Set up authentication headers
            let mut headers = HeaderMap::new();
            headers.insert(COOKIE, HeaderValue::from_str(&format!("{}{}", COOKIE_KEY, session_id))?);

            // Submit the report
            let res = match self.client.post(url).headers(headers).multipart(form).send().await {
                Ok(response) => response,
                Err(_) => return Ok(StatusCode::BAD_REQUEST), // Network error fallback
            };

            // Handle response and potential session expiration
            match res.status() {
                StatusCode::UNAUTHORIZED if local_retries < MAX_RETRY_COUNT => {
                    // Session expired - clear cache and retry
                    self.delete_session_id()?;
                    tokio::time::sleep(Duration::seconds(1).to_std()?).await;
                    local_retries += 1;
                    continue;
                }
                _ => return Ok(res.status()),
            }
        }
    }

    /// Submits a monthly summary report to SiServer.
    ///
    /// Sends aggregated monthly statistics to the SiServer API for organizational
    /// reporting and payroll integration. The report covers the entire month
    /// containing the specified date.
    ///
    /// ## Monthly Report Contents
    ///
    /// The system automatically generates a summary containing:
    /// - Total working hours for the month
    /// - Number of working days
    /// - Average daily productivity
    /// - Compliance with company policies
    ///
    /// ## Last Working Day Logic
    ///
    /// Monthly reports are typically submitted on the last working day of each month.
    /// The system can automatically detect this condition and prompt for submission.
    ///
    /// # Arguments
    ///
    /// * `date` - Any date within the target month for report generation
    ///
    /// # Returns
    ///
    /// Returns the HTTP status code from the API response.
    ///
    /// # Examples
    ///
    /// ```rust,no_run
    /// # use kasl::api::si::{Si, SiConfig};
    /// # use chrono::Local;
    /// # use anyhow::Result;
    /// # async fn example() -> Result<()> {
    /// # let config = SiConfig {
    /// #     login: "username".to_string(),
    /// #     auth_url: "https://auth.company.com".to_string(),
    /// #     api_url: "https://api.company.com".to_string(),
    /// # };
    /// let mut si = Si::new(&config);
    /// let today = Local::now().date_naive();
    ///
    /// if si.is_last_working_day_of_month(&today)? {
    ///     let status = si.send_monthly(&today).await?;
    ///     if status.is_success() {
    ///         println!("Monthly report submitted");
    ///     }
    /// }
    /// # Ok(())
    /// # }
    /// ```
    pub async fn send_monthly(&mut self, date: &NaiveDate) -> Result<StatusCode> {
        let mut local_retries = 0;
        loop {
            // Get valid session for API request
            let session_id = self.get_session_id().await?;
            let url = format!("{}/{}", self.config.api_url, MONTHLY_REPORT_URL);
            let (year, month) = (date.year(), date.month());

            // Prepare monthly report form data
            let form = multipart::Form::new().text("month", month.to_string()).text("year", year.to_string());

            // Set up authentication headers
            let mut headers = HeaderMap::new();
            headers.insert(COOKIE, HeaderValue::from_str(&format!("{}{}", COOKIE_KEY, session_id))?);

            // Submit the monthly report
            let res = match self.client.post(url).headers(headers).multipart(form).send().await {
                Ok(response) => response,
                Err(_) => return Ok(StatusCode::BAD_REQUEST), // Network error fallback
            };

            // Handle response and potential session expiration
            match res.status() {
                StatusCode::UNAUTHORIZED if local_retries < MAX_RETRY_COUNT => {
                    // Session expired - clear cache and retry
                    self.delete_session_id()?;
                    tokio::time::sleep(Duration::seconds(1).to_std()?).await;
                    local_retries += 1;
                    continue;
                }
                _ => return Ok(res.status()),
            }
        }
    }

    /// Fetches company rest dates and holidays for the specified year.
    ///
    /// This method retrieves the official company calendar including holidays,
    /// vacation days, and extended weekend periods. The data is used for accurate
    /// productivity calculations and report generation.
    ///
    /// ## Error Resilience
    ///
    /// This function prioritizes application stability over data completeness:
    /// - Network errors return empty results rather than failing
    /// - Authentication failures are logged but don't interrupt operation
    /// - API parsing errors result in empty calendar (graceful degradation)
    /// - Session failures are handled with automatic retry
    ///
    /// This design ensures that calendar integration enhances functionality
    /// without breaking core time tracking features when services are unavailable.
    ///
    /// ## Date Processing
    ///
    /// The API returns three categories of rest dates:
    /// - Regular holidays (national and company holidays)
    /// - Vacation dates (company-specific rest periods)
    /// - Weekend extensions (long weekend periods)
    ///
    /// All categories are combined into a single set for unified processing.
    ///
    /// # Arguments
    ///
    /// * `year` - Any date within the target year for calendar retrieval
    ///
    /// # Returns
    ///
    /// Returns a `HashSet<NaiveDate>` containing all rest dates for the year.
    /// Returns an empty set on any error to ensure graceful degradation.
    ///
    /// # Examples
    ///
    /// ```rust,no_run
    /// # use kasl::api::si::{Si, SiConfig};
    /// # use chrono::Local;
    /// # use anyhow::Result;
    /// # async fn example() -> Result<()> {
    /// # let config = SiConfig {
    /// #     login: "username".to_string(),
    /// #     auth_url: "https://auth.company.com".to_string(),
    /// #     api_url: "https://api.company.com".to_string(),
    /// # };
    /// let mut si = Si::new(&config);
    /// let this_year = Local::now().date_naive();
    ///
    /// let rest_dates = si.rest_dates(this_year).await?;
    /// println!("Found {} rest dates this year", rest_dates.len());
    ///
    /// // Check if a specific date is a rest day
    /// let today = Local::now().date_naive();
    /// if rest_dates.contains(&today) {
    ///     println!("Today is a company rest day");
    /// }
    /// # Ok(())
    /// # }
    /// ```
    pub async fn rest_dates(&mut self, year: NaiveDate) -> Result<HashSet<NaiveDate>> {
        let mut local_retries = 0;
        loop {
            // Get valid session for API request
            let session_id = match self.get_session_id().await {
                Ok(id) => id,
                Err(e) => {
                    msg_error!(Message::SiServerSessionFailed(e.to_string()));
                    return Ok(HashSet::new()); // Return empty set on session failure
                }
            };

            // Prepare rest dates request
            let url = format!("{}/{}", self.config.api_url, REST_DATES_URL);
            let form = multipart::Form::new().text("year", year.format("%Y").to_string());
            let mut headers = HeaderMap::new();
            headers.insert(COOKIE, HeaderValue::from_str(&format!("{}{}", COOKIE_KEY, session_id))?);

            // Request rest dates from API
            let res = match self.client.post(url).headers(headers).multipart(form).send().await {
                Ok(resp) => resp,
                Err(e) => {
                    msg_error!(Message::SiServerRestDatesFailed(e.to_string()));
                    return Ok(HashSet::new()); // Return empty set on network error
                }
            };

            // Handle response and potential session expiration
            match res.status() {
                StatusCode::UNAUTHORIZED if local_retries < MAX_RETRY_COUNT => {
                    // Session expired - clear cache and retry
                    self.delete_session_id()?;
                    local_retries += 1;
                    continue;
                }
                _ => {
                    // Process successful response or non-recoverable error
                    return match res.json::<RestDatesResponse>().await {
                        Ok(response) => Ok(response.unique_dates()?),
                        Err(e) => {
                            msg_error!(Message::SiServerRestDatesParsingFailed(e.to_string()));
                            Ok(HashSet::new()) // Return empty set on parsing error
                        }
                    };
                }
            }
        }
    }

    /// Determines if the specified date is the last working day of its month.
    ///
    /// This utility function calculates whether a given date represents the final
    /// working day in its month, which is useful for triggering monthly report
    /// submissions and other end-of-month processing.
    ///
    /// ## Algorithm
    ///
    /// The calculation process:
    /// 1. **Find Month End**: Determine the last calendar day of the month
    /// 2. **Weekend Adjustment**: Move backward from weekends to find working days
    /// 3. **Comparison**: Check if the input date matches the calculated last working day
    ///
    /// ## Limitations
    ///
    /// Currently only considers weekends (Saturday/Sunday) as non-working days.
    /// Future versions may integrate with the rest dates API to consider holidays
    /// and company-specific non-working days for more accurate calculations.
    ///
    /// # Arguments
    ///
    /// * `date` - The date to check against the last working day
    ///
    /// # Returns
    ///
    /// Returns `true` if the date is the last working day of its month,
    /// `false` otherwise.
    ///
    /// # Errors
    ///
    /// Currently cannot fail, but returns `Result` for consistency and
    /// future enhancement with holiday integration.
    ///
    /// # Examples
    ///
    /// ```rust,no_run
    /// # use kasl::api::si::{Si, SiConfig};
    /// # use chrono::NaiveDate;
    /// # use anyhow::Result;
    /// # fn example() -> Result<()> {
    /// # let config = SiConfig {
    /// #     login: "username".to_string(),
    /// #     auth_url: "https://auth.company.com".to_string(),
    /// #     api_url: "https://api.company.com".to_string(),
    /// # };
    /// let si = Si::new(&config);
    /// let date = NaiveDate::from_ymd_opt(2024, 1, 31).unwrap(); // January 31st
    ///
    /// if si.is_last_working_day_of_month(&date)? {
    ///     println!("Time to submit monthly report!");
    /// }
    /// # Ok(())
    /// # }
    /// ```
    pub fn is_last_working_day_of_month(&self, date: &NaiveDate) -> Result<bool> {
        let (year, month) = (date.year(), date.month());

        // Calculate the last day of the current month
        let mut last_day_of_month = NaiveDate::from_ymd_opt(year, month + 1, 1).unwrap().pred_opt().unwrap();

        // Move backward from weekends to find the last working day
        while matches!(last_day_of_month.weekday(), Weekday::Sat | Weekday::Sun) {
            last_day_of_month -= Duration::days(1);
        }

        // Check if the input date matches the calculated last working day
        Ok(date == &last_day_of_month)
    }
}

/// Configuration for SiServer API integration.
///
/// This structure holds the necessary information for connecting to internal
/// SiServer systems. Unlike other API integrations, SiServer requires separate
/// authentication and API endpoints due to its sophisticated security architecture.
///
/// ## Multi-Endpoint Architecture
///
/// SiServer uses different endpoints for different purposes:
/// - **Authentication URL**: LDAP authentication endpoint
/// - **API URL**: Main API endpoint for reports and data
/// - **Separation Benefits**: Enhanced security, load distribution, service isolation
///
/// ## Security Considerations
///
/// - Passwords are never stored in configuration files
/// - Only username and endpoints are persisted
/// - Session tokens are cached separately with encryption
/// - Double base64 password encoding for transmission security
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct SiConfig {
    /// Username for SiServer authentication.
    ///
    /// This should be the corporate username used for LDAP authentication.
    /// Typically matches the username used for other company systems.
    pub login: String,

    /// URL for the SiServer authentication endpoint.
    ///
    /// This endpoint handles LDAP authentication and token generation.
    /// Example: `https://auth.company.com`
    ///
    /// This is separate from the main API URL due to SiServer's security architecture.
    pub auth_url: String,

    /// Base URL for the main SiServer API endpoints.
    ///
    /// This endpoint handles report submission and data retrieval operations.
    /// Example: `https://api.company.com`
    ///
    /// All API operations (reports, calendar) use this base URL.
    pub api_url: String,
}

impl SiConfig {
    /// Returns the configuration module metadata for SiServer.
    ///
    /// Used by the configuration system to identify and manage
    /// SiServer-specific settings during interactive setup.
    ///
    /// # Returns
    ///
    /// A `ConfigModule` with SiServer identification information.
    pub fn module() -> ConfigModule {
        ConfigModule {
            key: "si".to_string(),
            name: "SiServer".to_string(),
        }
    }

    /// Runs an interactive configuration setup for SiServer integration.
    ///
    /// Prompts the user for SiServer connection details including username
    /// and both authentication and API endpoints. Uses existing configuration
    /// values as defaults if available.
    ///
    /// ## Interactive Prompts
    ///
    /// 1. **Username**: Corporate username for LDAP authentication
    /// 2. **Authentication URL**: LDAP endpoint for token generation
    /// 3. **API URL**: Main API endpoint for reports and data operations
    ///
    /// All prompts show existing values as defaults if configuration already
    /// exists, making it easy to update specific values without re-entering everything.
    ///
    /// ## Configuration Validation
    ///
    /// While this method doesn't validate actual connectivity, it provides
    /// helpful prompts to guide users toward correct configuration values
    /// for their corporate SiServer deployment.
    ///
    /// # Arguments
    ///
    /// * `config` - Existing SiServer configuration to use as defaults (if any)
    ///
    /// # Returns
    ///
    /// * `Result<Self>` - New SiServer configuration with user input
    ///
    /// # Errors
    ///
    /// Returns an error if:
    /// - Terminal input/output fails
    /// - User cancels the configuration process
    /// - Input validation fails
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// # use kasl::api::SiConfig;
    /// # use anyhow::Result;
    /// # fn example() -> Result<()> {
    /// let existing_config = Some(SiConfig {
    ///     login: "olduser".to_string(),
    ///     auth_url: "https://old-auth.com".to_string(),
    ///     api_url: "https://old-api.com".to_string(),
    /// });
    ///
    /// let new_config = SiConfig::init(&existing_config)?;
    /// # Ok(())
    /// # }
    /// ```
    pub fn init(config: &Option<SiConfig>) -> Result<Self> {
        // Use existing configuration as defaults, or create empty defaults
        let config = config.clone().unwrap_or(Self {
            login: "".to_string(),
            auth_url: "".to_string(),
            api_url: "".to_string(),
        });

        // Display configuration module header
        msg_print!(Message::ConfigModuleSiServer);

        // Interactive configuration with existing values as defaults
        Ok(Self {
            login: Input::with_theme(&ColorfulTheme::default())
                .with_prompt("Enter your SiServer login")
                .default(config.login)
                .interact_text()?,
            auth_url: Input::with_theme(&ColorfulTheme::default())
                .with_prompt("Enter your SiServer login URL")
                .default(config.auth_url)
                .interact_text()?,
            api_url: Input::with_theme(&ColorfulTheme::default())
                .with_prompt("Enter the SiServer API URL")
                .default(config.api_url)
                .interact_text()?,
        })
    }
}