---
phase: 01-foundation
plan: 02
type: execute
wave: 2
depends_on:
  - 01-01
files_modified:
  - SharepointToolbox/Core/Models/TenantProfile.cs
  - SharepointToolbox/Core/Models/OperationProgress.cs
  - SharepointToolbox/Core/Messages/TenantSwitchedMessage.cs
  - SharepointToolbox/Core/Messages/LanguageChangedMessage.cs
  - SharepointToolbox/Core/Helpers/SharePointPaginationHelper.cs
  - SharepointToolbox/Core/Helpers/ExecuteQueryRetryHelper.cs
  - SharepointToolbox/Infrastructure/Logging/LogPanelSink.cs
autonomous: true
requirements:
  - FOUND-05
  - FOUND-06
  - FOUND-07
  - FOUND-08
must_haves:
  truths:
    - "OperationProgress record is usable by all feature services for IProgress<T> reporting"
    - "TenantSwitchedMessage and LanguageChangedMessage are broadcast-ready via WeakReferenceMessenger"
    - "SharePointPaginationHelper iterates past 5,000 items using ListItemCollectionPosition"
    - "ExecuteQueryRetryHelper surfaces retry events as IProgress messages"
    - "LogPanelSink writes to a RichTextBox-targeted dispatcher-safe callback"
  artifacts:
    - path: "SharepointToolbox/Core/Models/OperationProgress.cs"
      provides: "Shared progress record used by all feature services"
      contains: "record OperationProgress"
    - path: "SharepointToolbox/Core/Models/TenantProfile.cs"
      provides: "Profile model matching JSON schema"
      contains: "TenantUrl"
    - path: "SharepointToolbox/Core/Helpers/SharePointPaginationHelper.cs"
      provides: "CSOM list pagination wrapping CamlQuery + ListItemCollectionPosition"
      contains: "ListItemCollectionPosition"
    - path: "SharepointToolbox/Core/Helpers/ExecuteQueryRetryHelper.cs"
      provides: "Retry wrapper for CSOM calls with throttle detection"
      contains: "ExecuteQueryRetryAsync"
    - path: "SharepointToolbox/Infrastructure/Logging/LogPanelSink.cs"
      provides: "Custom Serilog sink that writes to UI log panel"
      contains: "ILogEventSink"
  key_links:
    - from: "SharepointToolbox/Core/Helpers/SharePointPaginationHelper.cs"
      to: "Microsoft.SharePoint.Client.ListItemCollectionPosition"
      via: "PnP.Framework CSOM"
      pattern: "ListItemCollectionPosition"
    - from: "SharepointToolbox/Infrastructure/Logging/LogPanelSink.cs"
      to: "Application.Current.Dispatcher"
      via: "InvokeAsync for thread safety"
      pattern: "Dispatcher.InvokeAsync"
---

<objective>
Build the Core layer — models, messages, and infrastructure helpers — that every subsequent plan depends on. These are the contracts: no business logic, just types and patterns.

Purpose: All feature phases import OperationProgress, TenantProfile, the pagination helper, and the retry helper. Getting these right here means no rework in Phases 2-4.
Output: Core/Models, Core/Messages, Core/Helpers, Infrastructure/Logging directories with 7 files.
</objective>

<execution_context>
@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md
@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/phases/01-foundation/01-CONTEXT.md
@.planning/phases/01-foundation/01-RESEARCH.md
@.planning/phases/01-foundation/01-01-SUMMARY.md
</context>

<tasks>

<task type="auto">
  <name>Task 1: Core models and WeakReferenceMessenger messages</name>
  <files>
    SharepointToolbox/Core/Models/TenantProfile.cs,
    SharepointToolbox/Core/Models/OperationProgress.cs,
    SharepointToolbox/Core/Messages/TenantSwitchedMessage.cs,
    SharepointToolbox/Core/Messages/LanguageChangedMessage.cs
  </files>
  <action>
    Create directories: `Core/Models/`, `Core/Messages/`

    **TenantProfile.cs**
    ```csharp
    namespace SharepointToolbox.Core.Models;

    public class TenantProfile
    {
        public string Name { get; set; } = string.Empty;
        public string TenantUrl { get; set; } = string.Empty;
        public string ClientId { get; set; } = string.Empty;
    }
    ```
    Note: Plain class (not record) — mutable for JSON deserialization with System.Text.Json. Field names `Name`, `TenantUrl`, `ClientId` must match existing JSON schema exactly (case-insensitive by default in STJ but preserve casing for compatibility).

    **OperationProgress.cs**
    ```csharp
    namespace SharepointToolbox.Core.Models;

    public record OperationProgress(int Current, int Total, string Message)
    {
        public static OperationProgress Indeterminate(string message) =>
            new(0, 0, message);
    }
    ```

    **TenantSwitchedMessage.cs**
    ```csharp
    using CommunityToolkit.Mvvm.Messaging.Messages;
    using SharepointToolbox.Core.Models;

    namespace SharepointToolbox.Core.Messages;

    public sealed class TenantSwitchedMessage : ValueChangedMessage<TenantProfile>
    {
        public TenantSwitchedMessage(TenantProfile profile) : base(profile) { }
    }
    ```

    **LanguageChangedMessage.cs**
    ```csharp
    using CommunityToolkit.Mvvm.Messaging.Messages;

    namespace SharepointToolbox.Core.Messages;

    public sealed class LanguageChangedMessage : ValueChangedMessage<string>
    {
        public LanguageChangedMessage(string cultureCode) : base(cultureCode) { }
    }
    ```

    Run `dotnet build SharepointToolbox/SharepointToolbox.csproj` to verify no errors.
  </action>
  <verify>
    <automated>cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet build SharepointToolbox/SharepointToolbox.csproj 2>&1 | tail -5</automated>
  </verify>
  <done>Build succeeds. Four files created. TenantProfile fields match JSON schema. OperationProgress is a record with Indeterminate factory.</done>
</task>

<task type="auto">
  <name>Task 2: SharePointPaginationHelper, ExecuteQueryRetryHelper, LogPanelSink</name>
  <files>
    SharepointToolbox/Core/Helpers/SharePointPaginationHelper.cs,
    SharepointToolbox/Core/Helpers/ExecuteQueryRetryHelper.cs,
    SharepointToolbox/Infrastructure/Logging/LogPanelSink.cs
  </files>
  <action>
    Create directories: `Core/Helpers/`, `Infrastructure/Logging/`

    **SharePointPaginationHelper.cs**
    ```csharp
    using Microsoft.SharePoint.Client;
    using SharepointToolbox.Core.Models;

    namespace SharepointToolbox.Core.Helpers;

    public static class SharePointPaginationHelper
    {
        /// <summary>
        /// Enumerates all items in a SharePoint list, bypassing the 5,000-item threshold.
        /// Uses CamlQuery with RowLimit=2000 and ListItemCollectionPosition for pagination.
        /// Never call ExecuteQuery directly on a list — always use this helper.
        /// </summary>
        public static async IAsyncEnumerable<ListItem> GetAllItemsAsync(
            ClientContext ctx,
            List list,
            CamlQuery? baseQuery = null,
            CancellationToken ct = default)
        {
            var query = baseQuery ?? CamlQuery.CreateAllItemsQuery();
            query.ViewXml = BuildPagedViewXml(query.ViewXml, rowLimit: 2000);
            query.ListItemCollectionPosition = null;

            do
            {
                ct.ThrowIfCancellationRequested();
                var items = list.GetItems(query);
                ctx.Load(items);
                await ctx.ExecuteQueryAsync();

                foreach (var item in items)
                    yield return item;

                query.ListItemCollectionPosition = items.ListItemCollectionPosition;
            }
            while (query.ListItemCollectionPosition != null);
        }

        private static string BuildPagedViewXml(string? existingXml, int rowLimit)
        {
            // Inject or replace RowLimit in existing CAML, or create minimal view
            if (string.IsNullOrWhiteSpace(existingXml))
                return $"<View><RowLimit>{rowLimit}</RowLimit></View>";

            // Simple replacement approach — adequate for Phase 1
            if (existingXml.Contains("<RowLimit>", StringComparison.OrdinalIgnoreCase))
            {
                return System.Text.RegularExpressions.Regex.Replace(
                    existingXml, @"<RowLimit[^>]*>\d+</RowLimit>",
                    $"<RowLimit>{rowLimit}</RowLimit>",
                    System.Text.RegularExpressions.RegexOptions.IgnoreCase);
            }
            return existingXml.Replace("</View>", $"<RowLimit>{rowLimit}</RowLimit></View>",
                StringComparison.OrdinalIgnoreCase);
        }
    }
    ```

    **ExecuteQueryRetryHelper.cs**
    ```csharp
    using Microsoft.SharePoint.Client;
    using SharepointToolbox.Core.Models;

    namespace SharepointToolbox.Core.Helpers;

    public static class ExecuteQueryRetryHelper
    {
        private const int MaxRetries = 5;

        /// <summary>
        /// Executes a SharePoint query with automatic retry on throttle (429/503).
        /// Surfaces retry events via progress for user visibility ("Throttled — retrying in 30s…").
        /// </summary>
        public static async Task ExecuteQueryRetryAsync(
            ClientContext ctx,
            IProgress<OperationProgress>? progress = null,
            CancellationToken ct = default)
        {
            int attempt = 0;
            while (true)
            {
                ct.ThrowIfCancellationRequested();
                try
                {
                    await ctx.ExecuteQueryAsync();
                    return;
                }
                catch (Exception ex) when (IsThrottleException(ex) && attempt < MaxRetries)
                {
                    attempt++;
                    int delaySeconds = (int)Math.Pow(2, attempt) * 5; // 10, 20, 40, 80, 160s
                    progress?.Report(OperationProgress.Indeterminate(
                        $"Throttled by SharePoint — retrying in {delaySeconds}s (attempt {attempt}/{MaxRetries})…"));
                    await Task.Delay(TimeSpan.FromSeconds(delaySeconds), ct);
                }
            }
        }

        private static bool IsThrottleException(Exception ex)
        {
            var msg = ex.Message;
            return msg.Contains("429") || msg.Contains("503") ||
                   msg.Contains("throttl", StringComparison.OrdinalIgnoreCase);
        }
    }
    ```

    **LogPanelSink.cs**
    ```csharp
    using Serilog.Core;
    using Serilog.Events;
    using System.Windows;
    using System.Windows.Documents;
    using System.Windows.Media;
    using System.Windows.Controls;

    namespace SharepointToolbox.Infrastructure.Logging;

    /// <summary>
    /// Custom Serilog sink that writes timestamped, color-coded entries to a WPF RichTextBox.
    /// Format: HH:mm:ss [LEVEL] Message — green=info/success, orange=warning, red=error.
    /// All writes dispatch to the UI thread via Application.Current.Dispatcher.
    /// </summary>
    public class LogPanelSink : ILogEventSink
    {
        private readonly RichTextBox _richTextBox;

        public LogPanelSink(RichTextBox richTextBox)
        {
            _richTextBox = richTextBox;
        }

        public void Emit(LogEvent logEvent)
        {
            var message = logEvent.RenderMessage();
            var timestamp = logEvent.Timestamp.ToString("HH:mm:ss");
            var level = logEvent.Level.ToString().ToUpperInvariant()[..4]; // INFO, WARN, ERRO, FATL
            var text = $"{timestamp} [{level}] {message}";
            var color = GetColor(logEvent.Level);

            Application.Current?.Dispatcher.InvokeAsync(() =>
            {
                var para = new Paragraph(new Run(text) { Foreground = new SolidColorBrush(color) })
                {
                    Margin = new Thickness(0)
                };
                _richTextBox.Document.Blocks.Add(para);
                _richTextBox.ScrollToEnd();
            });
        }

        private static Color GetColor(LogEventLevel level) => level switch
        {
            LogEventLevel.Warning => Colors.Orange,
            LogEventLevel.Error or LogEventLevel.Fatal => Colors.Red,
            _ => Colors.LimeGreen
        };
    }
    ```

    Run `dotnet build SharepointToolbox/SharepointToolbox.csproj` to verify no errors.
  </action>
  <verify>
    <automated>cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet build SharepointToolbox/SharepointToolbox.csproj 2>&1 | tail -5</automated>
  </verify>
  <done>Build succeeds. SharePointPaginationHelper uses ListItemCollectionPosition. ExecuteQueryRetryHelper detects throttle exceptions. LogPanelSink dispatches to UI thread via Dispatcher.InvokeAsync.</done>
</task>

</tasks>

<verification>
- `dotnet build SharepointToolbox.sln` passes with 0 errors
- `SharepointToolbox/Core/Models/TenantProfile.cs` contains `TenantUrl` (not `TenantURL` or `Url`) to match JSON schema
- `SharePointPaginationHelper.cs` contains `ListItemCollectionPosition` and loop condition checking for null
- `ExecuteQueryRetryHelper.cs` contains exponential backoff and progress reporting
- `LogPanelSink.cs` contains `Dispatcher.InvokeAsync`
</verification>

<success_criteria>
All 7 Core/Infrastructure files created and compiling. Models match JSON schema field names. Pagination helper correctly loops until ListItemCollectionPosition is null.
</success_criteria>

<output>
After completion, create `.planning/phases/01-foundation/01-02-SUMMARY.md`
</output>
