arcgis 0.1.3

Type-safe Rust SDK for the ArcGIS REST API with compile-time guarantees
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
//! Advanced Feature Service query examples.
//!
//! This example demonstrates advanced Feature Service query capabilities:
//! - Statistical aggregations (COUNT, SUM, AVG, MIN, MAX with GROUP BY)
//! - Pagination patterns (manual pagination for large datasets)
//! - Feature count queries (efficient server-side counting)
//! - Advanced query parameters (distinct values, complex filters)
//! - Related record queries (find related records across tables)
//! - Top N features queries (top features by ranking within groups)
//! - Domain lookups (get valid coded values for fields)
//!
//! # Use Case: SF311 Service Request Analysis
//!
//! This example uses SF311 service requests to demonstrate:
//! 1. Calculating statistics by request type
//! 2. Paginating through large result sets efficiently
//! 3. Counting features without transferring data
//! 4. Using query_with_params for advanced query control
//! 5. Finding top 5 requests by district (optional - newer services)
//! 6. Related records queries (optional - requires relationship classes)
//! 7. Domain lookups (optional - requires domain definitions)
//!
//! # Prerequisites
//!
//! - None! This example uses a public ESRI sample service
//! - The service has 9,664+ SF311 service request records
//! - No authentication required (NoAuth)
//!
//! # Environment Variables
//!
//! None required for this example.
//!
//! # Running
//!
//! ```bash
//! # With debug logging to see all operations
//! RUST_LOG=debug cargo run --example advanced_queries
//!
//! # With info logging for cleaner output
//! RUST_LOG=info cargo run --example advanced_queries
//! ```

use anyhow::Result;
use arcgis::example_tracker::ExampleTracker;
use arcgis::{
    ArcGISClient, FeatureQueryParams, FeatureServiceClient, LayerId, NoAuth, ObjectId,
    RelatedRecordsParams, StatisticDefinition, StatisticType, TopFeaturesParams, TopFilter,
};
use tracing::instrument;

/// ESRI's sample SF311 incidents service (for demonstration).
///
/// This is a public service with 9,664+ service request incidents from San Francisco.
/// Fields: req_type (request type), district (neighborhood), status, address, etc.
const SAMPLE_SERVICE: &str =
    "https://sampleserver6.arcgisonline.com/arcgis/rest/services/SF311/FeatureServer";

#[tokio::main]
#[instrument]
async fn main() -> Result<()> {
    tracing::debug!("Initializing tracing subscriber");
    // Initialize tracing for structured logging
    tracing_subscriber::fmt()
        .with_env_filter(
            tracing_subscriber::EnvFilter::try_from_default_env()
                .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
        )
        .init();

    // Start accountability tracking
    let tracker = ExampleTracker::new("advanced_queries")
        .service_type("ExampleClient")
        .start();

    tracing::info!("🔍 Advanced Feature Service Query Examples");
    tracing::info!("Use Case: SF311 Service Request Analysis");
    tracing::debug!(service_url = %SAMPLE_SERVICE, "Using ESRI sample SF311 service");

    // Create client (NoAuth - public service)
    tracing::debug!("Creating ArcGIS client for public service");
    let client = ArcGISClient::new(NoAuth);
    let service = FeatureServiceClient::new(SAMPLE_SERVICE, &client);
    let layer_id = LayerId::new(0);

    tracing::info!("\n📊 Demonstrating Advanced Query Capabilities\n");

    // First test a simple query to verify authentication and that service has data
    tracing::debug!("Testing basic connectivity with simple query");
    let test_result = service
        .query(layer_id)
        .where_clause("1=1")
        .limit(10)
        .return_geometry(false)
        .execute()
        .await?;

    let feature_count = test_result.features().len();
    tracing::info!(
        features = feature_count,
        "Basic query successful - authentication working"
    );

    // Service should have some data for meaningful tests
    anyhow::ensure!(
        feature_count > 0,
        "Service returned no features - cannot demonstrate advanced queries. \
         Service may be empty or query may have failed silently."
    );

    // Demonstrate advanced query operations
    demonstrate_statistical_aggregations(&service, layer_id).await?;
    demonstrate_pagination_strategies(&service, layer_id).await?;
    demonstrate_feature_count(&service, layer_id).await?;
    demonstrate_query_with_params(&service, layer_id).await?;

    // Note: Advanced features require specific service configurations
    tracing::info!("\n⚠️  Note: Some advanced queries require newer service versions:");
    tracing::info!("   - queryTopFeatures (ArcGIS Enterprise 10.3+, requires newer service)");
    tracing::info!("   - queryRelatedRecords (requires relationship classes defined)");
    tracing::info!("   - queryDomains (requires domain/subtype definitions)");
    tracing::info!("\n   This example demonstrates statistical aggregations and pagination,");
    tracing::info!("   which work with all ArcGIS service versions.");

    tracing::info!("\n✅ All advanced query examples completed successfully!");
    print_best_practices();

    // Mark tracking as successful
    tracker.success();
    Ok(())
}

/// Demonstrates statistical aggregations with GROUP BY and HAVING clauses.
///
/// This function shows how to:
/// - Calculate multiple statistics (COUNT, SUM, AVG, MIN, MAX)
/// - Group results by one or more fields
/// - Filter aggregated results with HAVING clause
/// - Order aggregated results
#[instrument(skip(service), fields(layer_id = %layer_id))]
async fn demonstrate_statistical_aggregations(
    service: &FeatureServiceClient<'_>,
    layer_id: LayerId,
) -> Result<()> {
    tracing::info!("=== Example 1: Statistical Aggregations ===");
    tracing::info!("Calculate service request statistics by type");
    tracing::debug!("Creating statistic definitions for aggregation");

    // Define statistics to calculate
    let stats = vec![StatisticDefinition::new(
        StatisticType::Count,
        "objectid".to_string(),
        "total_requests".to_string(),
    )];

    tracing::debug!(stat_count = stats.len(), "Prepared statistic definitions");

    // Query with statistics and grouping
    tracing::debug!("Executing statistics query with GROUP BY");
    let result = service
        .query(layer_id)
        .where_clause("1=1")
        .statistics(stats)
        .group_by(&["req_type"])
        .order_by(&["total_requests DESC"])
        .return_geometry(false)
        .execute()
        .await?;

    let feature_count = result.features().len();
    tracing::info!(
        groups = feature_count,
        "Statistics calculated by request type"
    );
    tracing::debug!(
        exceeded_limit = result.exceeded_transfer_limit(),
        "Query completion status"
    );

    // Verify statistics query returned results
    anyhow::ensure!(
        feature_count > 0,
        "Statistics query returned no groups. Expected aggregated results by req_type field. \
         Service may not have data or req_type field may not exist."
    );

    // Display top results and verify they have required fields
    for (idx, feature) in result.features().iter().take(5).enumerate() {
        let req_type = feature.attributes().get("req_type");
        let count = feature.attributes().get("total_requests");

        // Verify aggregated results have expected fields
        anyhow::ensure!(
            count.is_some(),
            "Statistics result missing 'total_requests' field. \
             Aggregation may have failed or field name mismatch."
        );

        tracing::info!(
            rank = idx + 1,
            request_type = ?req_type,
            request_count = ?count,
            "Service request type statistics"
        );
    }

    tracing::debug!("Statistical aggregations demonstration complete");
    Ok(())
}

/// Demonstrates queryTopFeatures operation for ranking within groups.
///
/// This function shows how to:
/// - Define grouping fields
/// - Set top N count
/// - Order results within groups
/// - Apply filters to groups
///
/// **Note:** This operation requires ArcGIS Enterprise 10.3+ or ArcGIS Online.
/// The ESRI sample server used in this example doesn't support it, so this
/// function is kept for documentation but not called.
#[allow(dead_code)]
#[instrument(skip(service), fields(layer_id = %layer_id))]
async fn demonstrate_top_features(
    service: &FeatureServiceClient<'_>,
    layer_id: LayerId,
) -> Result<()> {
    tracing::info!("\n=== Example 2: Top N Features Query ===");
    tracing::info!("Find top 5 service requests by district");

    // Create top filter
    tracing::debug!("Creating TopFilter specification");
    let top_filter = TopFilter::new(
        vec!["district".to_string()],      // Group by district
        5,                                 // Top 5 from each district
        vec!["objectid DESC".to_string()], // Order by ID (proxy for recent)
    );

    tracing::debug!(
        group_by_fields = ?["district"],
        top_count = 5,
        "TopFilter configuration prepared"
    );

    // Build top features parameters
    tracing::debug!("Building TopFeaturesParams");
    let params = TopFeaturesParams::builder()
        .top_filter(top_filter)
        .where_("req_type IS NOT NULL".to_string())
        .out_fields(vec![
            "req_type".to_string(),
            "district".to_string(),
            "address".to_string(),
        ])
        .return_geometry(false)
        .f("json".to_string())
        .build()
        .expect("Valid top features params");

    tracing::debug!("Executing queryTopFeatures operation");
    let result = service.query_top_features(layer_id, params).await?;

    let feature_count = result.features().len();
    tracing::info!(
        total_features = feature_count,
        expected_max = 5 * 10, // 5 per district, ~10 districts
        "Top features query completed"
    );
    tracing::debug!(
        exceeded_limit = result.exceeded_transfer_limit(),
        "Query completion status"
    );

    // Verify top features query returned results
    anyhow::ensure!(
        feature_count > 0,
        "Top features query returned no results. Expected top 5 features per district. \
         Service may not have data or district field may not exist."
    );

    // Group and display results
    tracing::debug!("Processing and displaying top features by district");
    let mut by_district: std::collections::HashMap<String, Vec<_>> =
        std::collections::HashMap::new();

    for feature in result.features() {
        let district_value = feature.attributes().get("district");

        // Verify expected field exists
        anyhow::ensure!(
            district_value.is_some(),
            "Feature missing 'district' field. \
             Top features query requires this grouping field to exist."
        );

        if let Some(district) = district_value.and_then(|v| v.as_str()) {
            by_district
                .entry(district.to_string())
                .or_default()
                .push(feature);
        }
    }

    let district_count = by_district.len();
    tracing::info!(
        districts_found = district_count,
        "Grouping top features by district"
    );

    // Verify we got results for multiple districts
    anyhow::ensure!(
        district_count > 0,
        "No districts found in results. district field may be null or invalid."
    );

    for (district, features) in by_district.iter().take(3) {
        tracing::info!(
            district = %district,
            feature_count = features.len(),
            "Top requests in district"
        );

        for (idx, feature) in features.iter().take(3).enumerate() {
            let req_type = feature.attributes().get("req_type");
            let address = feature.attributes().get("address");

            tracing::debug!(
                district = %district,
                rank = idx + 1,
                request_type = ?req_type,
                address = ?address,
                "Service request detail"
            );
        }
    }

    tracing::debug!("Top features demonstration complete");
    Ok(())
}

/// Demonstrates pagination strategies for large datasets.
///
/// This function shows:
/// - Manual pagination with offset/limit
/// - Tracking total records fetched
/// - Detecting when pagination is complete
/// - Comparing manual vs automatic pagination
#[instrument(skip(service), fields(layer_id = %layer_id))]
async fn demonstrate_pagination_strategies(
    service: &FeatureServiceClient<'_>,
    layer_id: LayerId,
) -> Result<()> {
    tracing::info!("\n=== Example 3: Pagination Strategies ===");
    tracing::info!("Efficiently paginate through large result sets");

    let page_size = 100;
    let max_pages = 3; // Limit for demo

    tracing::debug!(
        page_size = page_size,
        max_pages = max_pages,
        "Starting manual pagination"
    );

    // Manual pagination
    let mut total_fetched = 0;
    let mut page_num = 0;

    loop {
        if page_num >= max_pages {
            tracing::debug!(
                pages_fetched = page_num,
                "Reached maximum page limit for demonstration"
            );
            break;
        }

        let offset = page_num * page_size;
        tracing::debug!(
            page = page_num,
            offset = offset,
            limit = page_size,
            "Fetching page"
        );

        let result = service
            .query(layer_id)
            .where_clause("1=1")
            .out_fields(&["req_type", "address"])
            .return_geometry(false)
            .limit(page_size)
            .offset(offset)
            .execute()
            .await?;

        let feature_count = result.features().len();
        total_fetched += feature_count;

        tracing::info!(
            page = page_num,
            features_in_page = feature_count,
            cumulative_total = total_fetched,
            exceeded_limit = result.exceeded_transfer_limit(),
            "Page retrieved"
        );

        // Check if we've fetched all available records
        if feature_count < page_size as usize || !*result.exceeded_transfer_limit() {
            tracing::debug!(
                feature_count = feature_count,
                page_size = page_size,
                exceeded_limit = result.exceeded_transfer_limit(),
                "Reached end of available data"
            );
            break;
        }

        page_num += 1;
    }

    tracing::info!(
        total_pages = page_num + 1,
        total_features = total_fetched,
        "Manual pagination complete"
    );

    // Verify manual pagination fetched data
    anyhow::ensure!(
        total_fetched > 0,
        "Manual pagination fetched no features. Service may be empty or query failed."
    );

    // Compare with automatic pagination
    tracing::debug!("Demonstrating automatic pagination with execute_all()");
    let auto_result = service
        .query(layer_id)
        .where_clause("1=1")
        .out_fields(&["req_type"])
        .return_geometry(false)
        .limit(page_size)
        .execute_all()
        .await?;

    let auto_total = auto_result.features().len();
    tracing::info!(
        auto_pagination_total = auto_total,
        manual_pagination_total = total_fetched,
        "Automatic pagination completed for comparison"
    );
    tracing::debug!(
        exceeded_limit = auto_result.exceeded_transfer_limit(),
        "Automatic pagination status"
    );

    // Verify automatic pagination also fetched data
    anyhow::ensure!(
        auto_total > 0,
        "Automatic pagination fetched no features. Service may be empty or query failed."
    );

    // Verify automatic pagination is consistent with manual (allowing for manual page limit)
    anyhow::ensure!(
        auto_total >= total_fetched,
        "Automatic pagination returned fewer features ({}) than manual ({}). This is unexpected.",
        auto_total,
        total_fetched
    );

    if auto_total > total_fetched {
        tracing::info!(
            additional_features = auto_total - total_fetched,
            "Automatic pagination fetched more features (manual was limited to {} pages)",
            max_pages
        );
    }

    tracing::debug!("Pagination strategies demonstration complete");
    Ok(())
}

/// Demonstrates efficient feature counting without returning feature data.
///
/// This function shows how to:
/// - Count all features in a layer
/// - Count features matching specific criteria
/// - Verify count-only queries are more efficient than full queries
///
/// `query_feature_count` returns only the count, making it much faster
/// and more bandwidth-efficient than querying features and counting client-side.
#[instrument(skip(service), fields(layer_id = %layer_id))]
async fn demonstrate_feature_count(
    service: &FeatureServiceClient<'_>,
    layer_id: LayerId,
) -> Result<()> {
    tracing::info!("\n=== Example 4: Feature Count Queries ===");
    tracing::info!("Efficiently count features without transferring data");
    tracing::debug!("Using query_feature_count for server-side counting");

    // Count all features
    tracing::debug!("Counting all features in layer");
    let total_count = service.query_feature_count(layer_id, "1=1").await?;

    tracing::info!(
        total_features = total_count,
        "Total features in layer (all records)"
    );

    // Verify service has features
    anyhow::ensure!(
        total_count > 0,
        "Layer contains no features. Expected at least some records for counting demonstration."
    );

    // Count features matching specific criteria
    tracing::debug!("Counting features with specific request type");
    let graffiti_count = service
        .query_feature_count(layer_id, "req_type LIKE 'Graffiti%'")
        .await?;

    tracing::info!(
        graffiti_requests = graffiti_count,
        "Features matching 'Graffiti' request type"
    );

    // Count with complex WHERE clause
    tracing::debug!("Counting features with complex criteria");
    let complex_count = service
        .query_feature_count(layer_id, "req_type IS NOT NULL AND district IS NOT NULL")
        .await?;

    tracing::info!(
        features_with_type_and_district = complex_count,
        "Features with both request type and district"
    );

    // Verify counts are reasonable
    anyhow::ensure!(
        complex_count <= total_count,
        "Filtered count ({}) cannot exceed total count ({})",
        complex_count,
        total_count
    );

    anyhow::ensure!(
        graffiti_count <= total_count,
        "Graffiti count ({}) cannot exceed total count ({})",
        graffiti_count,
        total_count
    );

    tracing::info!(
        "✅ Count queries verified: Graffiti={}, Complex={}, Total={}",
        graffiti_count,
        complex_count,
        total_count
    );

    tracing::debug!("Feature count demonstration complete");
    Ok(())
}

/// Demonstrates query_with_params for advanced query control.
///
/// This function shows how to:
/// - Use FeatureQueryParams builder for complex queries
/// - Query distinct values (unique field values)
/// - Combine multiple query parameters
/// - Control exactly which fields are returned
///
/// `query_with_params` gives lower-level control compared to the query builder,
/// useful when you need specific parameter combinations.
#[instrument(skip(service), fields(layer_id = %layer_id))]
async fn demonstrate_query_with_params(
    service: &FeatureServiceClient<'_>,
    layer_id: LayerId,
) -> Result<()> {
    tracing::info!("\n=== Example 5: Advanced Query with Params ===");
    tracing::info!("Use FeatureQueryParams for fine-grained query control");

    // Example 1: Query with specific field filtering (advanced params)
    tracing::debug!("Building FeatureQueryParams for selective field query");
    let selective_params = FeatureQueryParams::builder()
        .where_clause("req_type IS NOT NULL")
        .out_fields(vec!["req_type".to_string(), "district".to_string()])
        .return_geometry(false)
        .result_record_count(10u32) // Limit results
        .order_by_fields(vec!["req_type ASC".to_string()])
        .build()
        .expect("Valid query params");

    tracing::debug!("Executing query_with_params with selective fields");
    let selective_result = service
        .query_with_params(layer_id, selective_params)
        .await?;

    let result_count = selective_result.features().len();
    tracing::info!(
        features_returned = result_count,
        "Selective field query completed"
    );

    // Verify we got results
    anyhow::ensure!(
        result_count > 0,
        "Expected results from selective field query. Service may have no data."
    );

    // Display first few results
    tracing::info!("Sample results with selected fields:");
    for (idx, feature) in selective_result.features().iter().take(5).enumerate() {
        let req_type = feature.attributes().get("req_type");
        let district = feature.attributes().get("district");
        tracing::info!(
            item = idx + 1,
            request_type = ?req_type,
            district = ?district,
            "Selective field result"
        );

        // Verify field exists
        anyhow::ensure!(
            req_type.is_some(),
            "Selective query should return req_type field"
        );
    }

    // Example 2: Complex query with multiple parameters
    tracing::debug!("Building complex FeatureQueryParams with multiple filters");
    let complex_params = FeatureQueryParams::builder()
        .where_clause("district = 'Downtown' AND req_type IS NOT NULL")
        .out_fields(vec![
            "req_type".to_string(),
            "address".to_string(),
            "district".to_string(),
        ])
        .return_geometry(false)
        .result_record_count(10u32)
        .order_by_fields(vec!["req_type ASC".to_string()])
        .build()
        .expect("Valid query params");

    tracing::debug!("Executing complex query_with_params");
    let complex_result = service.query_with_params(layer_id, complex_params).await?;

    let result_count = complex_result.features().len();
    tracing::info!(
        downtown_requests = result_count,
        exceeded_limit = complex_result.exceeded_transfer_limit(),
        "Downtown district requests"
    );

    // Display results
    if result_count > 0 {
        tracing::info!("Sample Downtown requests:");
        for (idx, feature) in complex_result.features().iter().take(3).enumerate() {
            let req_type = feature.attributes().get("req_type");
            let address = feature.attributes().get("address");

            tracing::info!(
                item = idx + 1,
                request_type = ?req_type,
                address = ?address,
                "Downtown request"
            );

            // Verify expected fields are present
            anyhow::ensure!(
                req_type.is_some(),
                "Result missing req_type field from out_fields"
            );
        }

        tracing::info!(
            "✅ query_with_params verified: {} results returned",
            result_count
        );
    } else {
        tracing::warn!(
            "No results for Downtown district - this district may not exist in sample data. \
             This is acceptable for demonstration purposes."
        );
    }

    tracing::debug!("query_with_params demonstration complete");
    Ok(())
}

/// Demonstrates querying related records across relationship classes.
///
/// **Note:** This requires a service with relationship classes defined.
/// Not all services have relationships configured. The ESRI sample server
/// used in this example doesn't have relationships, so this function is
/// kept for documentation but not called.
///
/// This function shows how to:
/// - Query related records for specific features
/// - Access related records grouped by parent object ID
/// - Navigate relationship classes
#[allow(dead_code)]
#[instrument(skip(service), fields(layer_id = %layer_id))]
async fn demonstrate_related_records(
    service: &FeatureServiceClient<'_>,
    layer_id: LayerId,
) -> Result<()> {
    tracing::info!("\n=== Example 4: Related Records Query ===");
    tracing::info!("Find evidence records related to crime incidents");
    tracing::debug!("Note: Requires service with relationship classes defined");

    // First, get some incident IDs
    tracing::debug!("Fetching sample incident IDs");
    let incidents = service
        .query(layer_id)
        .where_clause("1=1")
        .ids_only(true)
        .limit(5)
        .execute()
        .await?;

    let object_ids: Vec<ObjectId> = incidents
        .features()
        .iter()
        .filter_map(|f| {
            f.attributes()
                .get("objectid")
                .and_then(|v| v.as_i64())
                .and_then(|id| {
                    if id >= 0 {
                        Some(ObjectId::new(id as u32))
                    } else {
                        None
                    }
                })
        })
        .collect();

    tracing::debug!(
        incident_count = object_ids.len(),
        "Retrieved incident IDs for related records query"
    );

    anyhow::ensure!(
        !object_ids.is_empty(),
        "No object IDs found for related records query. Service may be empty or objectid field missing."
    );

    // Build related records params
    tracing::debug!("Building RelatedRecordsParams");
    let params = RelatedRecordsParams::builder()
        .object_ids(object_ids.clone())
        .relationship_id(0u32) // Relationship ID (service-specific)
        .out_fields(vec!["*".to_string()])
        .return_geometry(false)
        .build()
        .expect("Valid related records params");

    tracing::debug!(
        object_id_count = object_ids.len(),
        relationship_id = 0,
        "Executing queryRelatedRecords operation"
    );

    // Query related records
    let result = service.query_related_records(layer_id, params).await?;

    let group_count = result.related_record_groups().len();
    tracing::info!(groups = group_count, "Related records query completed");

    // Display results
    for group in result.related_record_groups().iter().take(3) {
        let related_count = group.related_records().len();

        tracing::info!(
            incident_id = ?group.object_id(),
            related_records = related_count,
            "Found related evidence"
        );

        for (idx, record) in group.related_records().iter().take(2).enumerate() {
            tracing::debug!(
                incident_id = ?group.object_id(),
                evidence_num = idx + 1,
                attributes = ?record.attributes(),
                "Evidence record detail"
            );
        }
    }

    tracing::debug!("Related records demonstration complete");
    Ok(())
}

/// Demonstrates querying field domains and subtypes.
///
/// **Note:** This requires a service with domains/subtypes configured.
/// The ESRI sample server used in this example doesn't have domains,
/// so this function is kept for documentation but not called.
///
/// This function shows how to:
/// - Query coded value domains
/// - Access domain values for specific layers
/// - Get subtype information
#[allow(dead_code)]
#[instrument(skip(service))]
async fn demonstrate_domain_lookups(service: &FeatureServiceClient<'_>) -> Result<()> {
    tracing::info!("\n=== Example 5: Domain Lookups ===");
    tracing::info!("Query valid domain values for crime types");
    tracing::debug!("Note: Requires service with domain/subtype definitions");

    let layers = vec![LayerId::new(0)];

    tracing::debug!(layer_count = layers.len(), "Querying domains for layers");

    let result = service.query_domains(layers).await?;

    let layer_count = result.layers().len();
    tracing::info!(layers_with_domains = layer_count, "Domain query completed");

    anyhow::ensure!(
        layer_count > 0,
        "Domain query returned no layers. Service may not support domains or query failed."
    );

    for layer_info in result.layers() {
        let domain_count = layer_info.domains().len();
        let subtype_count = layer_info.subtypes().as_ref().map(|s| s.len()).unwrap_or(0);

        tracing::info!(
            layer_id = ?layer_info.id(),
            domains = domain_count,
            subtypes = subtype_count,
            "Layer domain information"
        );

        // Display first few domains
        for (field_name, domain) in layer_info.domains().iter().take(3) {
            tracing::debug!(
                field = %field_name,
                domain_name = %domain.name(),
                domain_type = ?domain.domain_type(),
                "Domain definition"
            );

            // Display coded values if available
            if let Some(coded_values) = domain.coded_values() {
                let value_count = coded_values.len();
                tracing::debug!(
                    field = %field_name,
                    coded_value_count = value_count,
                    "Coded values available"
                );

                for (idx, coded_value) in coded_values.iter().take(3).enumerate() {
                    tracing::debug!(
                        field = %field_name,
                        value_num = idx + 1,
                        code = ?coded_value.code(),
                        name = %coded_value.name(),
                        "Valid domain value"
                    );
                }
            }
        }
    }

    tracing::debug!("Domain lookups demonstration complete");
    Ok(())
}

/// Prints best practices for advanced queries.
fn print_best_practices() {
    tracing::info!("\n💡 Advanced Query Best Practices:");
    tracing::info!("");
    tracing::info!("📊 Statistical Aggregations:");
    tracing::info!("   - Use statistics queries instead of client-side aggregation");
    tracing::info!("   - GROUP BY reduces data transfer and improves performance");
    tracing::info!("   - HAVING filters aggregated results efficiently");
    tracing::info!("   - Combine with ORDER BY to sort aggregated results");
    tracing::info!("");
    tracing::info!("🏆 Top Features:");
    tracing::info!("   - Use queryTopFeatures for ranking within groups");
    tracing::info!("   - Specify clear ORDER BY for deterministic results");
    tracing::info!("   - Consider filtering with WHERE before grouping");
    tracing::info!("   - Top queries reduce result size vs full query + sort");
    tracing::info!("");
    tracing::info!("📄 Pagination:");
    tracing::info!("   - Manual pagination: Full control over page fetching");
    tracing::info!("   - Automatic pagination: Convenient for smaller datasets");
    tracing::info!("   - Use resultOffset/resultRecordCount for manual control");
    tracing::info!("   - Monitor exceededTransferLimit to detect more data");
    tracing::info!("   - Page size affects performance: 100-1000 is typical");
    tracing::info!("");
    tracing::info!("🔢 Feature Counting:");
    tracing::info!("   - Use query_feature_count for server-side counting");
    tracing::info!("   - Much faster than querying all features and counting");
    tracing::info!("   - Reduces bandwidth - only count is transferred");
    tracing::info!("   - Supports WHERE clauses for filtered counts");
    tracing::info!("   - Perfect for pagination (get total before fetching pages)");
    tracing::info!("");
    tracing::info!("🔧 Advanced Parameters:");
    tracing::info!("   - query_with_params: Lower-level control vs query builder");
    tracing::info!("   - returnDistinctValues: Get unique field values efficiently");
    tracing::info!("   - Use specific out_fields to reduce response size");
    tracing::info!("   - Combine multiple parameters for complex queries");
    tracing::info!("   - Distinct queries useful for populating dropdowns");
    tracing::info!("");
    tracing::info!("🔗 Relationships:");
    tracing::info!("   - Query related records instead of joining tables");
    tracing::info!("   - Results are grouped by source object ID");
    tracing::info!("   - Filter related records with definitionExpression");
    tracing::info!("   - Reduce network traffic with targeted out_fields");
    tracing::info!("");
    tracing::info!("🏷️  Domains:");
    tracing::info!("   - Query domains once, cache for form validation");
    tracing::info!("   - Use coded values for dropdown lists");
    tracing::info!("   - Subtypes affect valid domain values per feature");
    tracing::info!("   - Domain queries don't count against rate limits");
    tracing::info!("");
    tracing::info!("⚡ Performance:");
    tracing::info!("   - Server-side aggregation >> client-side processing");
    tracing::info!("   - Return only needed fields with out_fields");
    tracing::info!("   - Skip geometry when not needed (return_geometry: false)");
    tracing::info!("   - Use appropriate page sizes for your use case");
    tracing::info!("   - Consider PBF format for large datasets");
}