ehds-logging 0.4.0

The European logging software component of an EHR system under Regulation (EU) 2025/327: the Annex II 3.2 access record, the Article 14(1) categories and the map that classifies an access, and the record written as an IHE BALP AuditEvent
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
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
// SPDX-FileCopyrightText: Cadasto B.V.
// SPDX-License-Identifier: BUSL-1.1

//! The access record: what the logging component records "on every access
//! event or group of events" (Regulation (EU) 2025/327 Annex II 3.2).
//!
//! Annex II 3.2 asks for (a) the healthcare provider or other individuals
//! that accessed the data, (b) the specific natural person or persons, (c)
//! the categories of data accessed, (d) the time and date, and (e) the
//! origin or origins of the data. An [`AccessRecord`] carries each, and
//! beside them the data subject whose data were accessed, the purpose of
//! use the access was made for, which Art 9(2) asks the patient be told, and
//! the policies it was authorised under, such as a data permit (Art 68). A
//! record whose accessor declared an emergency purpose carries the mark
//! Art 11(5) asks for ([`emergency`](crate::emergency)).
//!
//! The accessor names how the recording system bound it to a party
//! ([`Binding`]), its token's actor chain ([`Actor`]) and the representation
//! a representative acts under ([`Representation`]); an accessor no party
//! here names is what a contact point or a federation relays ([`Relayed`]),
//! held at construction to the rules [`RelayedError`] lists. Which fields
//! carry the binding is our own design: Annex II 3.2 names what a record
//! identifies, not how.
//!
//! A record names the patient and the person who accessed their data, so
//! `Debug` shows neither, nor any id the classification read: a record
//! leaves through an [`AccessSink`](crate::sink::AccessSink) alone.

use std::collections::BTreeSet;
use std::fmt;
use std::net::IpAddr;

use jiff::Timestamp;
use secrecy::SecretString;

use crate::classify::Classification;
use crate::emergency::Emergency;
use crate::label::Labels;
use crate::retention::Retention;
use crate::transformation::Transformation;

/// What the access did to the data.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Action {
    /// A query over the data, an ad hoc or a stored one.
    Query,
    /// A read of one resource.
    Read,
    /// The creation of a resource.
    Create,
    /// The update of a resource.
    Update,
    /// The deletion of a resource.
    Delete,
}

/// How the access ended.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Outcome {
    /// The data were delivered or written as asked.
    Success,
    /// The access was answered with a failure, or with less than asked.
    MinorFailure,
    /// The access failed on the side that records it.
    SeriousFailure,
    /// The access failed and left the recording system unavailable.
    MajorFailure,
}

/// One purpose of use the accessor declared: a code and the system that
/// defines it.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Purpose {
    /// The code system, when one is named.
    pub system: Option<String>,
    /// The code.
    pub code: String,
}

/// Who acts behind the token an access was authenticated by.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Acting {
    /// The natural person the token's `sub` names.
    Person,
    /// A client application, which the recording system admits to personal
    /// data only for the professional the token names; kept so a stored
    /// record that names it stays readable.
    Client,
    /// A client application acting for the person the token names, by a
    /// delegation such as an RFC 8693 token exchange.
    Delegated,
    /// A verified person or organisation of another party, whom a contact
    /// point or a federation relays ([`Relayed`]).
    External,
    /// A client application acting for nobody, as the recording system's
    /// own machine user.
    System,
}

impl Acting {
    /// The code a record writes the acting mode as: `person`, `client`,
    /// `delegated`, `external` or `system`.
    #[must_use]
    pub const fn code(self) -> &'static str {
        match self {
            Self::Person => "person",
            Self::Client => "client",
            Self::Delegated => "delegated",
            Self::External => "external",
            Self::System => "system",
        }
    }
}

/// An assurance level of an electronic identification means, as Regulation
/// (EU) No 910/2014 Art 8(2) names them, lowest first.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum AssuranceLevel {
    /// Level low, Art 8(2)(a).
    Low,
    /// Level substantial, Art 8(2)(b).
    Substantial,
    /// Level high, Art 8(2)(c).
    High,
}

impl AssuranceLevel {
    /// The code a record writes the level as: `low`, `substantial` or
    /// `high`.
    #[must_use]
    pub const fn code(self) -> &'static str {
        match self {
            Self::Low => "low",
            Self::Substantial => "substantial",
            Self::High => "high",
        }
    }
}

/// The professional's identification, as the token the access was
/// authenticated by states it.
///
/// `Debug` shows which members are present and none of their values.
#[derive(Clone, Default, PartialEq, Eq)]
pub struct Professional {
    /// The professional's name.
    pub name: Option<String>,
    /// The identifier their national authority issued them as a health
    /// professional.
    pub identifier: Option<String>,
}

impl fmt::Debug for Professional {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Professional")
            .field("name", &self.name.is_some())
            .field("identifier", &self.identifier.is_some())
            .finish()
    }
}

/// A coded value, such as a professional role: a code and the system that
/// defines it.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Coded {
    /// The code system, when one is named.
    pub system: Option<String>,
    /// The code.
    pub code: String,
}

/// How a relayed accessor reached the recording system: the issuer of the
/// token that relays them, and under which law.
///
/// The two cases fall under different law, so one field never carries both.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Via {
    /// A MyHealth@EU national contact point for digital health, whose
    /// relaying Implementing Regulation (EU) 2026/2099 Art 7 binds.
    ContactPoint {
        /// The issuer of the contact point's token.
        issuer: String,
    },
    /// A domestic federation issuer, such as a national gateway, which
    /// 2026/2099 does not bind: the access is made under the agreement of
    /// the federation ([`Agreement`]).
    Federation {
        /// The issuer of the federation's token.
        issuer: String,
    },
}

impl Via {
    /// The code a record writes the case as: `contact-point` or
    /// `federation`.
    #[must_use]
    pub const fn code(&self) -> &'static str {
        match self {
            Self::ContactPoint { .. } => "contact-point",
            Self::Federation { .. } => "federation",
        }
    }

    /// The issuer of the relaying token.
    #[must_use]
    pub fn issuer(&self) -> &str {
        match self {
            Self::ContactPoint { issuer } | Self::Federation { issuer } => issuer,
        }
    }
}

/// The SAML `NameID` of the professional's identity assertion, with the
/// assertion's `Issuer` (eHDSI SAML Profile 10.0.0 §2.1).
///
/// `Debug` shows that a service provider id is present and no value.
#[derive(Clone, PartialEq, Eq)]
pub struct NameId {
    /// `Subject/NameID`: the professional's identifier, which the issuer
    /// "MUST guarantee ... can be long-term tracked back to an individual
    /// person".
    pub value: String,
    /// `NameID@SPProvidedID`, when the assertion names one.
    pub sp_provided_id: Option<String>,
    /// `Issuer`: the endpoint of the service that issued the assertion.
    pub issuer: String,
}

impl NameId {
    /// The user identifier the eHDSI Audit Trail Profile 10.1.0 §2.3.41
    /// fixes: `SPProvidedID<NameID@Issuer>`, the `SPProvidedID` empty when
    /// the assertion names none.
    #[must_use]
    pub fn user_id(&self) -> String {
        format!(
            "{}<{}@{}>",
            self.sp_provided_id.as_deref().unwrap_or_default(),
            self.value,
            self.issuer
        )
    }
}

impl fmt::Debug for NameId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("NameId")
            .field("sp_provided_id", &self.sp_provided_id.is_some())
            .finish_non_exhaustive()
    }
}

/// How the professional authenticated, as its `AuthnContextClassRef` class.
///
/// One of the seven two-factor classes the eHDSI SAML Profile 10.0.0 §2.1
/// admits, recorded in place of an assurance level, which the assertion
/// does not carry.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AuthenticationMethod {
    /// `urn:oasis:names:tc:SAML:2.0:ac:classes:MobileTwoFactorUnregistered`.
    MobileTwoFactorUnregistered,
    /// `urn:oasis:names:tc:SAML:2.0:ac:classes:MobileTwoFactorContract`.
    MobileTwoFactorContract,
    /// `urn:oasis:names:tc:SAML:2.0:ac:classes:X509`.
    X509,
    /// `urn:oasis:names:tc:SAML:2.0:ac:classes:SPKI`.
    Spki,
    /// `urn:oasis:names:tc:SAML:2.0:ac:classes:SmartcardPKI`.
    SmartcardPki,
    /// `urn:oasis:names:tc:SAML:2.0:ac:classes:SoftwarePKI`.
    SoftwarePki,
    /// `urn:oasis:names:tc:SAML:2.0:ac:classes:TLSClient`.
    TlsClient,
}

impl AuthenticationMethod {
    /// Every method, in the order the profile lists them.
    const ALL: [Self; 7] = [
        Self::MobileTwoFactorUnregistered,
        Self::MobileTwoFactorContract,
        Self::X509,
        Self::Spki,
        Self::SmartcardPki,
        Self::SoftwarePki,
        Self::TlsClient,
    ];

    /// The `AuthnContextClassRef` URI.
    #[must_use]
    pub const fn uri(self) -> &'static str {
        match self {
            Self::MobileTwoFactorUnregistered => {
                "urn:oasis:names:tc:SAML:2.0:ac:classes:MobileTwoFactorUnregistered"
            }
            Self::MobileTwoFactorContract => {
                "urn:oasis:names:tc:SAML:2.0:ac:classes:MobileTwoFactorContract"
            }
            Self::X509 => "urn:oasis:names:tc:SAML:2.0:ac:classes:X509",
            Self::Spki => "urn:oasis:names:tc:SAML:2.0:ac:classes:SPKI",
            Self::SmartcardPki => "urn:oasis:names:tc:SAML:2.0:ac:classes:SmartcardPKI",
            Self::SoftwarePki => "urn:oasis:names:tc:SAML:2.0:ac:classes:SoftwarePKI",
            Self::TlsClient => "urn:oasis:names:tc:SAML:2.0:ac:classes:TLSClient",
        }
    }

    /// The method whose `AuthnContextClassRef` is `uri`, or `None` for a
    /// class the profile does not admit.
    #[must_use]
    pub fn from_uri(uri: &str) -> Option<Self> {
        Self::ALL.into_iter().find(|method| method.uri() == uri)
    }
}

/// What a contact point or a federation relays: the health professional and
/// the healthcare provider of another party, as the relaying issuer asserts
/// them.
///
/// The recording system verified the relaying issuer, never the
/// professional: Implementing Regulation (EU) 2026/2099 Art 6(1) and (2)
/// give their identification and authentication to the entity the Member
/// State of the professional lists, and a federation agreement gives them to
/// its member. [`Relayed`] is built from these parts, which it holds to the
/// rules [`RelayedError`] names.
#[derive(Clone, PartialEq, Eq)]
pub struct RelayedParts {
    /// How the accessor reached the recording system.
    pub via: Via,
    /// `country_code`: the ISO 3166-1 alpha-2 code of the Member State that
    /// issued the professional's identification data (2026/2099 Annex
    /// Table 1).
    pub country_code: String,
    /// The health professional (Annex Table 1), or `None` for an
    /// organisation a federation agreement admits without a named person.
    pub professional: Option<RelayedProfessional>,
    /// The healthcare provider (Annex Table 2).
    pub provider: RelayedProvider,
    /// The agreement the access is made under, which every federation
    /// access names (GDPR Art 5(2), Art 30(1)(b)).
    pub agreement: Option<Agreement>,
    /// The point of care, by name: the XSPA `locality` of the identity
    /// assertion (eHDSI SAML Profile 10.0.0 §2.3).
    pub point_of_care: Option<String>,
    /// The `@ID` of the professional's identity assertion, the `IdA`.
    pub identity_assertion_id: Option<String>,
    /// The `@ID` of the treatment relationship confirmation assertion (TRC).
    pub trc_assertion_id: Option<String>,
    /// The request message's UUID, the WS-Addressing `MessageID`, by which
    /// the record joins the contact point's Patient Privacy audit entry
    /// (eHDSI Audit Trail Profile 10.1.0 §2.3.12 to §2.3.32).
    pub message_id: Option<String>,
    /// The contact points the request passed, each by the CN of its TLS
    /// certificate (eHDSI Audit Trail Profile 10.1.0 §2.3.14, §2.3.15).
    pub gateways: Gateways,
}

/// The contact points a relayed request passed, each by the CN of its TLS
/// certificate (eHDSI Audit Trail Profile 10.1.0 §2.3.14, §2.3.15).
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Gateways {
    /// The service consumer NCP, in the professional's country, which
    /// triggered the operation.
    pub consumer: Option<String>,
    /// The service provider NCP, in the patient's country, which processed
    /// it.
    pub provider: Option<String>,
}

/// The federation agreement a relayed access is made under (GDPR Art 5(2)
/// and Art 30(1)(b)).
///
/// `Debug` shows the access class and the counts, never a value.
#[derive(Clone, PartialEq, Eq)]
pub struct Agreement {
    /// The agreement's name.
    pub name: String,
    /// The agreement's version.
    pub version: String,
    /// Each legal ground the agreement rests on, coded: the GDPR Art 6(1)
    /// and Art 9(2) points and the national provision.
    pub legal_ground: Vec<Coded>,
    /// The reference of the consent decision taken for the patient, when
    /// the ground is consent.
    pub consent: Option<String>,
    /// How an organisation without a named person accesses the data, which
    /// an agreement that admits one declares.
    pub access_class: Option<AccessClass>,
}

impl fmt::Debug for Agreement {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Agreement")
            .field("legal_ground", &self.legal_ground.len())
            .field("consent", &self.consent.is_some())
            .field("access_class", &self.access_class)
            .finish_non_exhaustive()
    }
}

/// How an organisation without a named person accesses the data under its
/// agreement (no specification governs the classes: our own design).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum AccessClass {
    /// A system of the organisation, with no person viewing the data.
    Automated,
    /// A person views the data at the requester, who keeps the log that
    /// names them, joined to this record by an attested correlation id.
    Chained,
}

impl AccessClass {
    /// The code a record writes the class as: `automated` or `chained`.
    #[must_use]
    pub const fn code(self) -> &'static str {
        match self {
            Self::Automated => "automated",
            Self::Chained => "chained",
        }
    }
}

/// Why [`RelayedParts`] are no relayed accessor.
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum RelayedError {
    /// A contact point relayed no professional: the MyHealth@EU
    /// Requirements Catalogue 10.0.0 requirement 01 asks for the HP's
    /// information, whose absence is `ERROR_HPI_NO_INFORMATION`.
    #[error("a contact point relayed no health professional")]
    ContactPointWithoutProfessional,
    /// A federation access names no agreement.
    #[error("a federation access names no agreement")]
    FederationWithoutAgreement,
    /// An organisation without a named person is relayed under an agreement
    /// that declares no access class for it.
    #[error("an organisation without a named person has no access class")]
    OrganisationWithoutAccessClass,
    /// A contact point's provider is named by no `urn:oid:` or `urn:hl7ii:`
    /// `organization-id` (eHDSI SAML Profile 10.0.0 §2.3).
    #[error("the provider's organization-id is no urn:oid: or urn:hl7ii: identifier")]
    ProviderIdentifierForm,
    /// A contact point relays a clerical support worker, ISCO `44`, and not
    /// whom they act for, which eHDSI SAML Profile 10.0.0 §2.3 makes
    /// mandatory.
    #[error("a clerical support worker is relayed without on-behalf-of")]
    OnBehalfOfMissing,
}

/// The ISCO code system the eHDSI roles are a subset of (eHDSI SAML Profile
/// 10.0.0 §2.3).
pub const ISCO: &str = "2.16.840.1.113883.2.9.6.2.7";

/// What a contact point or a federation relays, held to its rules.
///
/// `Debug` shows how it was relayed and the country, and none of the other
/// values.
#[derive(Clone, PartialEq, Eq)]
pub struct Relayed(RelayedParts);

impl Relayed {
    /// The parts of this relayed accessor.
    #[must_use]
    pub fn as_parts(&self) -> &RelayedParts {
        &self.0
    }

    /// The parts of this relayed accessor, taken back.
    #[must_use]
    pub fn into_parts(self) -> RelayedParts {
        self.0
    }

    /// Whether the professional is named, or an organisation accessed the
    /// data without a named person.
    #[must_use]
    pub fn names_a_professional(&self) -> bool {
        self.0.professional.is_some()
    }
}

impl TryFrom<RelayedParts> for Relayed {
    type Error = RelayedError;

    fn try_from(parts: RelayedParts) -> Result<Self, Self::Error> {
        match (&parts.via, &parts.professional) {
            (Via::ContactPoint { .. }, None) => {
                return Err(RelayedError::ContactPointWithoutProfessional);
            }
            (Via::ContactPoint { .. }, Some(professional)) => {
                let identifier = &parts.provider.identifier;
                if !identifier.starts_with("urn:oid:") && !identifier.starts_with("urn:hl7ii:") {
                    return Err(RelayedError::ProviderIdentifierForm);
                }
                let clerical = professional
                    .roles
                    .iter()
                    .any(|role| role.code == "44" && role.system.as_deref() == Some(ISCO));
                if clerical && professional.on_behalf_of.is_none() {
                    return Err(RelayedError::OnBehalfOfMissing);
                }
            }
            (Via::Federation { .. }, professional) => {
                let Some(agreement) = &parts.agreement else {
                    return Err(RelayedError::FederationWithoutAgreement);
                };
                if professional.is_none() && agreement.access_class.is_none() {
                    return Err(RelayedError::OrganisationWithoutAccessClass);
                }
            }
        }
        Ok(Self(parts))
    }
}

impl fmt::Debug for Relayed {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Relayed")
            .field("via", &self.0.via.code())
            .field("country_code", &self.0.country_code)
            .field("professional", &self.0.professional)
            .field("provider", &self.0.provider)
            .field("agreement", &self.0.agreement)
            .finish_non_exhaustive()
    }
}

impl fmt::Debug for RelayedParts {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("RelayedParts")
            .field("via", &self.via.code())
            .field("country_code", &self.country_code)
            .finish_non_exhaustive()
    }
}

/// The health professional a contact point or a federation relays
/// (2026/2099 Annex Table 1, with the eHDSI SAML Profile 10.0.0 §2.3
/// attributes).
#[derive(Clone, PartialEq, Eq)]
pub struct RelayedProfessional {
    /// The full name, the XSPA `subject-id`.
    pub name: String,
    /// `family_name`, for an issuer that sends it apart.
    pub family_name: Option<String>,
    /// `given_name`, for an issuer that sends it apart.
    pub given_name: Option<String>,
    /// `hp_identifier`, unique among the identifiers the Member State where
    /// the professional is registered issues.
    pub identifier: String,
    /// `issuing_authority_name`: the agency that issued `hp_identifier`.
    pub issuing_authority: String,
    /// `hp_professional_role`, as the codes the relaying issuer states,
    /// such as the ISCO role of the XACML `role` attribute.
    pub roles: Vec<Coded>,
    /// `healthcare_provider_identifier`: "An identifier of the healthcare
    /// provider organisation or healthcare facility where the health
    /// professional is providing treatment" (Annex Table 1).
    pub healthcare_provider_identifier: Option<String>,
    /// The identity assertion's `NameID`, which the DICOM rendering names
    /// the requester by (eHDSI Audit Trail Profile 10.1.0 §2.3.41).
    pub name_id: Option<NameId>,
    /// The `AuthnContextClassRef` of the professional's authentication.
    pub authentication_method: Option<AuthenticationMethod>,
    /// `on-behalf-of`: the role of the person the professional acts for,
    /// mandatory for an ISCO `44` clerical support worker.
    pub on_behalf_of: Option<Coded>,
    /// The XSPA `hl7:permission` URIs the issuer grants.
    pub permissions: Vec<String>,
}

impl fmt::Debug for RelayedProfessional {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("RelayedProfessional")
            .field("roles", &self.roles.len())
            .field("name_id", &self.name_id)
            .field("authentication_method", &self.authentication_method)
            .field("on_behalf_of", &self.on_behalf_of.is_some())
            .field("permissions", &self.permissions.len())
            .finish_non_exhaustive()
    }
}

/// The healthcare provider a contact point or a federation relays
/// (2026/2099 Annex Table 2, with the eHDSI SAML Profile 10.0.0 §2.3
/// attributes), where the professional provides the treatment.
#[derive(Clone, PartialEq, Eq)]
pub struct RelayedProvider {
    /// `healthcare_provider_identifier`, unique in the Member State that
    /// issued it; from a contact point the XSPA `organization-id`, a
    /// `urn:oid:` or `urn:hl7ii:` identifier.
    pub identifier: String,
    /// `issuing_authority_name`: the agency that issued the identifier.
    pub issuing_authority: String,
    /// `healthcare_provider_name`, or the XSPA `organization`, when named.
    pub name: Option<String>,
    /// `healthcare_provider_address`: the official registered full address,
    /// when the issuer states it.
    pub address: Option<String>,
    /// The eHDSI `healthcare-facility-type`, such as `Hospital`.
    pub facility_type: Option<Coded>,
    /// The XCA `homeCommunityId` of the provider.
    pub home_community_id: Option<String>,
    /// The organisation type a national profile asks for, such as the
    /// Dutch one.
    pub organisation_type: Option<Coded>,
}

impl fmt::Debug for RelayedProvider {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("RelayedProvider")
            .field("facility_type", &self.facility_type)
            .field("organisation_type", &self.organisation_type)
            .finish_non_exhaustive()
    }
}

/// A reference to a party in a demographic store, the openEHR `PARTY_REF`
/// triple.
///
/// `Debug` shows the type and none of the other values.
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct PartyRef {
    /// `namespace`.
    pub namespace: String,
    /// `type`, such as `PERSON` or `GROUP`.
    pub kind: String,
    /// `id`.
    pub id: String,
}

impl fmt::Debug for PartyRef {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("PartyRef")
            .field("kind", &self.kind)
            .finish_non_exhaustive()
    }
}

/// The party a person was bound to, with the groups they belong to.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BoundParty {
    /// The party.
    pub party: PartyRef,
    /// Each group the person belongs to, as the lookup matched it.
    pub groups: Vec<PartyRef>,
}

/// How the recording system bound the authenticated caller to a party, one
/// outcome per record (no specification governs the outcomes: our own
/// design).
///
/// The party is named on `bound`, `provisional`, `refused-inactive` and
/// `refused-abandoned`, and on no other outcome.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Binding {
    /// `bound`: a person bound to a complete party.
    Bound {
        /// The party and its groups.
        party: BoundParty,
        /// Whether the person is a guest of the issuer.
        guest: bool,
    },
    /// `provisional`: a person bound to a party not yet complete.
    Provisional {
        /// The party and its groups.
        party: BoundParty,
    },
    /// `external`: a verified person of another party ([`Relayed`]).
    External,
    /// `organisation`: an organisation under an agreement, with no named
    /// person.
    Organisation,
    /// `system`: a client acting for nobody.
    System,
    /// `self`: the patient, by a patient token.
    PartySelf,
    /// `refused-unbound`: no party carries the login.
    RefusedUnbound,
    /// `refused-ambiguous`: two or more parties carry the login.
    RefusedAmbiguous,
    /// `refused-inactive`: the party is inactive.
    RefusedInactive {
        /// The party.
        party: PartyRef,
    },
    /// `refused-abandoned`: the party is abandoned.
    RefusedAbandoned {
        /// The party.
        party: PartyRef,
    },
    /// `refused-key-claim-missing`: the token lacks the claim the binding
    /// keys on.
    RefusedKeyClaimMissing {
        /// The claim's name.
        claim: String,
    },
    /// `refused-groups-unavailable`: the group claim is unreadable or over
    /// its limit.
    RefusedGroupsUnavailable,
    /// `refused-guest`: a guest of the issuer.
    RefusedGuest,
    /// `refused-external-incomplete`: a federation token lacks or garbles a
    /// field it must carry.
    RefusedExternalIncomplete {
        /// The names of the fields.
        fields: Vec<String>,
    },
    /// `refused-trc-patient-mismatch`: the request's patient is not the
    /// treatment relationship confirmation's, recorded as a security event.
    RefusedTrcPatientMismatch,
}

impl Binding {
    /// The code a record writes the outcome as.
    #[must_use]
    pub const fn code(&self) -> &'static str {
        match self {
            Self::Bound { .. } => "bound",
            Self::Provisional { .. } => "provisional",
            Self::External => "external",
            Self::Organisation => "organisation",
            Self::System => "system",
            Self::PartySelf => "self",
            Self::RefusedUnbound => "refused-unbound",
            Self::RefusedAmbiguous => "refused-ambiguous",
            Self::RefusedInactive { .. } => "refused-inactive",
            Self::RefusedAbandoned { .. } => "refused-abandoned",
            Self::RefusedKeyClaimMissing { .. } => "refused-key-claim-missing",
            Self::RefusedGroupsUnavailable => "refused-groups-unavailable",
            Self::RefusedGuest => "refused-guest",
            Self::RefusedExternalIncomplete { .. } => "refused-external-incomplete",
            Self::RefusedTrcPatientMismatch => "refused-trc-patient-mismatch",
        }
    }

    /// The party the outcome names, when it names one.
    #[must_use]
    pub const fn party(&self) -> Option<&PartyRef> {
        match self {
            Self::Bound { party, .. } | Self::Provisional { party } => Some(&party.party),
            Self::RefusedInactive { party } | Self::RefusedAbandoned { party } => Some(party),
            _ => None,
        }
    }

    /// The groups the bound person belongs to.
    #[must_use]
    pub fn groups(&self) -> &[PartyRef] {
        match self {
            Self::Bound { party, .. } | Self::Provisional { party } => &party.groups,
            _ => &[],
        }
    }

    /// Whether the outcome is a `bound` guest of the issuer.
    #[must_use]
    pub const fn is_guest(&self) -> bool {
        matches!(self, Self::Bound { guest: true, .. })
    }

    /// Whether the outcome refused the request.
    #[must_use]
    pub const fn is_refusal(&self) -> bool {
        matches!(
            self,
            Self::RefusedUnbound
                | Self::RefusedAmbiguous
                | Self::RefusedInactive { .. }
                | Self::RefusedAbandoned { .. }
                | Self::RefusedKeyClaimMissing { .. }
                | Self::RefusedGroupsUnavailable
                | Self::RefusedGuest
                | Self::RefusedExternalIncomplete { .. }
                | Self::RefusedTrcPatientMismatch
        )
    }

    /// Whether the outcome is recorded as a security event.
    #[must_use]
    pub const fn is_security_event(&self) -> bool {
        matches!(self, Self::RefusedTrcPatientMismatch)
    }
}

/// One actor of a token's actor chain (RFC 8693 §4.1 `act`), recorded and
/// never decided on.
///
/// `Debug` shows that an issuer is named and no value.
#[derive(Clone, PartialEq, Eq)]
pub struct Actor {
    /// The actor's issuer, when the chain names one.
    pub issuer: Option<String>,
    /// The actor's subject.
    pub subject: String,
}

impl fmt::Debug for Actor {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Actor")
            .field("issuer", &self.issuer.is_some())
            .finish_non_exhaustive()
    }
}

/// Whom a representative acts for, and on which ground.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Representation {
    /// The person represented.
    pub represented: PartyRef,
    /// The ground of the representation.
    pub ground: Coded,
}

/// Who accessed the data (Annex II 3.2(a), (b)).
///
/// The person, as the token the access was authenticated by names them, or
/// as a local account names them when no token did, the client they used,
/// and the provider they act for.
///
/// Each value is the authentication's, as the recording system verified it:
/// the record infers none, so the issuer, the client, the acting mode and the
/// assurance level are absent unless the authentication established them.
#[derive(Clone, PartialEq, Eq)]
pub struct Accessor {
    /// The issuer that vouched for the person (`iss`), when a token named
    /// them; `None` for a user authenticated otherwise, such as by HTTP
    /// Basic.
    pub issuer: Option<String>,
    /// The person (`sub`, or the local account).
    pub subject: String,
    /// The client application they used (`client_id`), when a token named
    /// one.
    pub client_id: Option<String>,
    /// The audience the token named the recording system by.
    pub audience: Option<String>,
    /// The healthcare provider or other organisation the person acts for.
    pub provider: Option<String>,
    /// Who acts behind the token: the person, or a client application, when
    /// the recording system established it.
    pub acting: Option<Acting>,
    /// The roles or authorities the person acted under, such as the security
    /// roles of the recording system, which the record writes at
    /// `agent[user].role` (BALP 1.1.4 §3:5.7.5.4, the IUA `subject_role`).
    pub roles: Vec<Coded>,
    /// The identity of the access token (`jti`), when it names one.
    pub token_id: Option<String>,
    /// The assurance level the authentication reached, when the recording
    /// system established one from the token.
    pub assurance: Option<AssuranceLevel>,
    /// The professional the token names (3.2(b)), who acts in person or
    /// whom a client acts for.
    pub professional: Professional,
    /// Another identification of the person the token states beside its
    /// `sub`, such as a professional number a claim of the deployment's
    /// naming carries.
    pub alt_id: Option<String>,
    /// Every purpose of use the token declares.
    pub purposes: Vec<Purpose>,
    /// The identifier of every policy or plan the access was authorised
    /// under beside the token, such as the data permit of a health data
    /// access body (Art 68) a secondary-use caller declares it acts under,
    /// in order. The BALP record writes each at `agent[user].policy` (FHIR R4
    /// `AuditEvent.agent.policy`).
    pub policies: Vec<String>,
    /// The professional and the provider of another party a contact point
    /// or a federation relays, marked as asserted by it, when the client is
    /// one (Implementing Regulation (EU) 2026/2099 Art 7, or a federation
    /// agreement).
    pub relayed: Option<Relayed>,
    /// How the recording system bound the caller to a party, with the party
    /// and its groups where the outcome names them, when it bound at all.
    pub binding: Option<Binding>,
    /// The token's actor chain, outermost first: the relaying gateway, the
    /// contact point and the client, as RFC 8693 §4.1 nests them.
    pub actors: Vec<Actor>,
    /// Whom the person acts for as a representative, and on which ground.
    pub represents: Option<Representation>,
}

impl Accessor {
    /// The party the binding names, when it names one.
    #[must_use]
    pub fn party(&self) -> Option<&PartyRef> {
        self.binding.as_ref().and_then(Binding::party)
    }
}

impl fmt::Debug for Accessor {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Accessor")
            .field("provider", &self.provider.is_some())
            .field("acting", &self.acting)
            .field("roles", &self.roles)
            .field("token_id", &self.token_id.is_some())
            .field("assurance", &self.assurance)
            .field("professional", &self.professional)
            .field("alt_id", &self.alt_id.is_some())
            .field("purposes", &self.purposes)
            .field("policies", &self.policies.len())
            .field("relayed", &self.relayed)
            .field("binding", &self.binding)
            .field("actors", &self.actors)
            .field("represents", &self.represents)
            .finish_non_exhaustive()
    }
}

/// The patient an access concerned, by the identifier the request named and
/// the namespace that issued it; `Debug` shows the namespace alone.
#[derive(Debug, Clone)]
pub struct PatientIdentifier {
    /// The issuing namespace, when one is known: a URI, which the record
    /// writes as the identifier's system, or another name, such as an openEHR
    /// `PARTY_REF.namespace`, which it writes beside the identifier.
    pub namespace: Option<String>,
    /// The identifier.
    pub value: SecretString,
}

/// Whether the patient behind an `ehr_id` is named, so a search by the
/// patient's identifier finds the access (Art 9(1)).
///
/// `Debug` shows the outcome and how many identifiers were found, never one.
#[derive(Clone)]
#[non_exhaustive]
pub enum PatientLookup {
    /// The request named the patient, so the record names them already.
    RequestNamed,
    /// The identity service holds these identifiers for the patient.
    Found(Vec<PatientIdentifier>),
    /// The identity service holds no identifier for the patient in a
    /// namespace asked.
    NotFound,
    /// The identity service could not be asked, or did not answer.
    Unavailable,
    /// The recording system names no namespace to ask in.
    NotConfigured,
    /// The recording system has no identity service that names a patient
    /// by an `ehr_id`.
    Unsupported,
}

impl PatientLookup {
    /// The outcome as a record writes it: `request-named`, `found`,
    /// `not-found`, `unavailable`, `not-configured` or `unsupported`.
    #[must_use]
    pub const fn code(&self) -> &'static str {
        match self {
            Self::RequestNamed => "request-named",
            Self::Found(_) => "found",
            Self::NotFound => "not-found",
            Self::Unavailable => "unavailable",
            Self::NotConfigured => "not-configured",
            Self::Unsupported => "unsupported",
        }
    }
}

impl fmt::Debug for PatientLookup {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Found(found) => write!(f, "Found({})", found.len()),
            other => f.write_str(other.code()),
        }
    }
}

/// One `ehr_id` an access reached, at the endpoint that holds it, with
/// whether the patient it belongs to is named.
#[derive(Clone)]
pub struct EhrAt {
    /// The endpoint.
    pub endpoint: String,
    /// The `ehr_id` at that endpoint.
    pub ehr_id: String,
    /// Whether the patient behind the `ehr_id` is named.
    pub patient: PatientLookup,
}

impl fmt::Debug for EhrAt {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("EhrAt")
            .field("endpoint", &self.endpoint)
            .field("patient", &self.patient)
            .finish_non_exhaustive()
    }
}

/// Whose data an access concerned: the patient the request named, each
/// `ehr_id` the access reached, and, for an access over many patients' data
/// that names none of them, the scope it read.
#[derive(Clone, Default)]
pub struct DataSubject {
    /// The patient, when the request named one.
    pub patient: Option<PatientIdentifier>,
    /// The `ehr_id`s the access reached.
    pub ehrs: Vec<EhrAt>,
    /// The scope of an access over many patients' data whose record names
    /// none of them: a population access.
    pub population: Option<Population>,
}

impl fmt::Debug for DataSubject {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("DataSubject")
            .field("patient", &self.patient.is_some())
            .field("ehrs", &self.ehrs.len())
            .field("population", &self.population)
            .finish()
    }
}

/// Why the record of an access over many patients' data names none of the
/// `ehr_id`s whose records it read.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum PopulationReason {
    /// The query projects no `ehr_id`, so no delivered row says whose
    /// record it came from.
    NotProjected,
    /// The delivered rows name more `ehr_id`s than a record names.
    OverLimit,
    /// No row was delivered, so no row names an `ehr_id`.
    NotDelivered,
    /// A delivered `ehr_id` was in no origin's own answer, so the record
    /// cannot say which origin holds it.
    Unattributed,
}

impl PopulationReason {
    /// The reason as a record writes it: `not-projected`, `over-limit`,
    /// `not-delivered` or `unattributed`.
    #[must_use]
    pub const fn code(self) -> &'static str {
        match self {
            Self::NotProjected => "not-projected",
            Self::OverLimit => "over-limit",
            Self::NotDelivered => "not-delivered",
            Self::Unattributed => "unattributed",
        }
    }
}

/// The scope of a population access: what the access read, when the record
/// names none of the persons whose records it read.
///
/// The origins the access asked are the record's origins. A person's
/// search finds the record through an origin that holds their record, and
/// it says that a query over that origin ran, never that it read their
/// record. `Debug` shows the reason and the counts, never an id.
#[derive(Clone, PartialEq, Eq)]
pub struct Population {
    /// Why the record names no `ehr_id`.
    pub reason: PopulationReason,
    /// How many `ehr_id`s the delivered rows named, when they were read.
    pub ehrs: Option<usize>,
    /// The most `ehr_id`s a record names.
    pub limit: usize,
    /// The template ids the query constrains its data to.
    pub templates: BTreeSet<String>,
    /// The archetype ids the query constrains its data to.
    pub archetypes: BTreeSet<String>,
}

impl fmt::Debug for Population {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Population")
            .field("reason", &self.reason)
            .field("ehrs", &self.ehrs)
            .field("limit", &self.limit)
            .field("templates", &self.templates.len())
            .field("archetypes", &self.archetypes.len())
            .finish()
    }
}

/// One origin of the data (Annex II 3.2(e)).
///
/// An endpoint the access asked, with how it answered, or the system stored
/// data came from, such as the originating system an openEHR `FEEDER_AUDIT`
/// names, or both.
#[derive(Clone, PartialEq, Eq)]
pub struct Origin {
    /// The endpoint the access asked, as the registry names it, when it
    /// asked one.
    pub endpoint: Option<String>,
    /// The node behind it.
    pub node: Option<String>,
    /// The openEHR `system_id` of the node, or of the system stored data
    /// came from.
    pub system_id: Option<String>,
    /// How the endpoint answered, such as `active` or `node-error`, when an
    /// endpoint was asked.
    pub status: Option<String>,
    /// The rows or objects it contributed, when counted.
    pub rows: Option<usize>,
    /// The version a write left at the endpoint, as its answer named it, or
    /// `None` for an access that wrote nothing there.
    pub version: Option<WrittenVersion>,
    /// The `ehr_id` an EHR create left at the endpoint, as its answer named
    /// it, or `None` for an access that created no EHR there.
    pub created_ehr: Option<CreatedEhr>,
    /// The categories of what it contributed, when told apart from the
    /// other origins'.
    pub categories: Option<Classification>,
}

impl Origin {
    /// What the origin is keyed by for retention and labels: its endpoint,
    /// or its `system_id` when it names no endpoint.
    #[must_use]
    pub fn key(&self) -> Option<&str> {
        self.endpoint.as_deref().or(self.system_id.as_deref())
    }
}

impl fmt::Debug for Origin {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Origin")
            .field("endpoint", &self.endpoint)
            .field("node", &self.node)
            .field("status", &self.status)
            .field("rows", &self.rows)
            .field("version", &self.version)
            .field("created_ehr", &self.created_ehr)
            .field("categories", &self.categories)
            .finish_non_exhaustive()
    }
}

/// The version a write left at its origin, which is the data a write
/// accessed (Annex II 3.2), as the origin's answer named it.
///
/// The recording system holds the version id to the `OBJECT_VERSION_ID`
/// form before it is recorded, so a value the answer carried in its place,
/// whatever it holds, never reaches the record.
#[derive(Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum WrittenVersion {
    /// The answer named this `OBJECT_VERSION_ID`.
    Named(String),
    /// The answer named no version, so the record cannot tell which one the
    /// write left.
    Unnamed,
    /// The answer named a version that is no `OBJECT_VERSION_ID`, so the
    /// record cannot tell which one the write left.
    Malformed,
}

impl WrittenVersion {
    /// The version id, when the answer named one.
    #[must_use]
    pub fn named(&self) -> Option<&str> {
        match self {
            Self::Named(version) => Some(version),
            Self::Unnamed | Self::Malformed => None,
        }
    }

    /// Why the record cannot tell the version, as a record writes it:
    /// `unnamed` or `malformed`, or `None` when the answer named it.
    #[must_use]
    pub fn unread(&self) -> Option<&'static str> {
        match self {
            Self::Named(_) => None,
            Self::Unnamed => Some("unnamed"),
            Self::Malformed => Some("malformed"),
        }
    }
}

impl fmt::Debug for WrittenVersion {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Named(_) => f.write_str("Named(..)"),
            Self::Unnamed => f.write_str("Unnamed"),
            Self::Malformed => f.write_str("Malformed"),
        }
    }
}

/// The `ehr_id` of the EHR a create left at its origin, as the origin's
/// answer named it.
///
/// The recording system holds the `ehr_id` to the `HIER_OBJECT_ID` form
/// before it is recorded, so a value the answer carried in its place,
/// whatever it holds, never reaches the record.
#[derive(Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum CreatedEhr {
    /// The answer named this `ehr_id`.
    Named(String),
    /// The answer named no `ehr_id`, so the record cannot tell which EHR the
    /// create left.
    Unnamed,
    /// The answer named an `ehr_id` that is no `HIER_OBJECT_ID`, or two
    /// that differ, so the record cannot tell which EHR the create left.
    Malformed,
}

impl CreatedEhr {
    /// The `ehr_id`, when the answer named one.
    #[must_use]
    pub fn named(&self) -> Option<&str> {
        match self {
            Self::Named(ehr_id) => Some(ehr_id),
            Self::Unnamed | Self::Malformed => None,
        }
    }

    /// Why the record cannot tell the `ehr_id`, as a record writes it:
    /// `unnamed` or `malformed`, or `None` when the answer named it.
    #[must_use]
    pub fn unread(&self) -> Option<&'static str> {
        match self {
            Self::Named(_) => None,
            Self::Unnamed => Some("unnamed"),
            Self::Malformed => Some("malformed"),
        }
    }
}

impl fmt::Debug for CreatedEhr {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Named(_) => f.write_str("Named(..)"),
            Self::Unnamed => f.write_str("Unnamed"),
            Self::Malformed => f.write_str("Malformed"),
        }
    }
}

/// What the access asked for.
#[derive(Clone)]
pub struct Request {
    /// The id the recording system knows the request by, which its own log
    /// names too, when the request carried one.
    pub id: Option<String>,
    /// The operation, such as an ITS-REST `operationId`.
    pub operation: String,
    /// The code system the operation is a code of, such as the system of the
    /// ITS-REST operation ids: when it is named, the record writes the
    /// operation as a `subtype` in it.
    pub operation_system: Option<String>,
    /// The resource the request addressed, when it addressed one: a FHIR
    /// literal reference (`[type]/[id]`) to a resource the recording system
    /// serves, or an id it issues, such as an openEHR version uid, which the
    /// BALP record names by an identifier (`balp::ObjectIds`, feature
    /// `balp`).
    pub resource: Option<String>,
    /// The query text, for a query.
    pub query: Option<SecretString>,
    /// The name of the stored query, for a stored-query execution.
    pub stored_query: Option<String>,
    /// The address the request came from, when known.
    pub client_address: Option<IpAddr>,
    /// The identifier the client correlates the request by in its own
    /// log, such as a national contact point's exchange log, with where the
    /// recording system read it, when the client sent one.
    pub correlation: Option<Correlation>,
}

/// The identifier a client correlates a request by in its own log, and where
/// the recording system read it.
///
/// `Debug` shows the source and not the value.
#[derive(Clone, PartialEq, Eq)]
pub struct Correlation {
    /// The identifier.
    pub value: String,
    /// Where it was read.
    pub source: CorrelationSource,
}

impl fmt::Debug for Correlation {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Correlation")
            .field("source", &self.source)
            .finish_non_exhaustive()
    }
}

/// Where a correlation identifier was read (no specification governs the
/// sources: our own design).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum CorrelationSource {
    /// A claim of the token the recording system's authorization server
    /// issued, which it attests.
    TokenClaim,
    /// A member of the token's `authorization_details`, which the
    /// authorization server attests.
    AuthorizationDetails,
    /// A request header, which nobody attests.
    Header,
}

impl CorrelationSource {
    /// The code a record writes the source as: `token-claim`,
    /// `authorization-details` or `header`.
    #[must_use]
    pub const fn code(self) -> &'static str {
        match self {
            Self::TokenClaim => "token-claim",
            Self::AuthorizationDetails => "authorization-details",
            Self::Header => "header",
        }
    }

    /// Whether the recording system's authorization server attests the
    /// source.
    #[must_use]
    pub const fn is_attested(self) -> bool {
        matches!(self, Self::TokenClaim | Self::AuthorizationDetails)
    }
}

impl fmt::Debug for Request {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Request")
            .field("id", &self.id)
            .field("operation", &self.operation)
            .field("query", &self.query.is_some())
            .field("correlation", &self.correlation)
            .finish_non_exhaustive()
    }
}

/// One access event (Annex II 3.2).
#[derive(Clone)]
pub struct AccessRecord {
    /// What the access did.
    pub action: Action,
    /// When it ended (3.2(d)).
    pub recorded: Timestamp,
    /// How it ended.
    pub outcome: Outcome,
    /// Free text on how it ended, when the recording system gives one.
    pub outcome_description: Option<String>,
    /// Who accessed the data (3.2(a), (b)), with their purposes of use.
    pub accessor: Accessor,
    /// Whose data.
    pub subject: DataSubject,
    /// The categories of the data accessed (3.2(c)).
    pub categories: Classification,
    /// The rows or objects delivered to the accessor, when counted.
    pub delivered: Option<usize>,
    /// The origins of the data (3.2(e)), in the order the access asked them.
    pub origins: Vec<Origin>,
    /// How many distinct origins the access reached, when `origins` names
    /// fewer, such as when the recording system bounds one record's origins.
    pub origin_count: Option<usize>,
    /// What the access asked for.
    pub request: Request,
    /// How long the record is kept (Art 9(2), Annex II 3.4).
    pub retention: Retention,
    /// The security labels the record carries for its categories and its
    /// origins, so the store that holds it can limit who reads it (Annex II
    /// 3.4).
    pub labels: Labels,
    /// The mark of an access the accessor declared, through its purpose of
    /// use, as made to protect the vital interests of the data subject (Art
    /// 11(5)), when it did.
    pub emergency: Option<Emergency>,
    /// Who inserted the information the access wrote under the data
    /// subject's own grant, which the written data marks as inserted by
    /// them (Art 5), when it did.
    pub inserted: Option<Insertion>,
    /// Each semantic transformation the access applied between openEHR and
    /// the exchange format (eHealth Network General Guidelines Release 3.1
    /// Art 6(3)), in the order it applied them.
    pub transformations: Vec<Transformation>,
}

/// Why a record is not one a sink stores.
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum InvalidRecord {
    /// An organisation accessed the data under a `chained` access class
    /// with no correlation id the recording system's authorization server
    /// attested, so the record cannot join the requester's log that names
    /// the person who viewed the data.
    #[error("a chained organisation access has no attested correlation id")]
    ChainedWithoutAttestedCorrelation,
}

impl AccessRecord {
    /// Holds the record to the rules a sink stores it under.
    ///
    /// # Errors
    ///
    /// An [`InvalidRecord`] naming the rule the record breaks.
    pub fn validate(&self) -> Result<(), InvalidRecord> {
        let chained = self.accessor.relayed.as_ref().is_some_and(|relayed| {
            let parts = relayed.as_parts();
            parts.professional.is_none()
                && parts
                    .agreement
                    .as_ref()
                    .is_some_and(|agreement| agreement.access_class == Some(AccessClass::Chained))
        });
        let attested = self
            .request
            .correlation
            .as_ref()
            .is_some_and(|correlation| correlation.source.is_attested());
        if chained && !attested {
            return Err(InvalidRecord::ChainedWithoutAttestedCorrelation);
        }
        Ok(())
    }
}

/// Who inserted the information a write carried under the data subject's
/// own grant, as the written data marks it (Art 5).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Insertion {
    /// The data subject, in person.
    Subject,
    /// A natural person who acts for the data subject, a proxy or a legal
    /// representative (Art 4(2)), whom the written data names.
    Representative,
}

impl fmt::Debug for AccessRecord {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("AccessRecord")
            .field("action", &self.action)
            .field("recorded", &self.recorded)
            .field("outcome", &self.outcome)
            .field("outcome_description", &self.outcome_description.is_some())
            .field("accessor", &self.accessor)
            .field("subject", &self.subject)
            .field("categories", &self.categories)
            .field("delivered", &self.delivered)
            .field("origins", &self.origins)
            .field("origin_count", &self.origin_count)
            .field("request", &self.request)
            .field("retention", &self.retention)
            .field("labels", &self.labels)
            .field("emergency", &self.emergency)
            .field("inserted", &self.inserted)
            .field("transformations", &self.transformations)
            .finish()
    }
}