---
phase: 02-permissions
plan: 04
type: execute
wave: 2
depends_on:
  - 02-02
files_modified:
  - SharepointToolbox/Services/Export/CsvExportService.cs
  - SharepointToolbox/Services/Export/HtmlExportService.cs
autonomous: true
requirements:
  - PERM-05
  - PERM-06

must_haves:
  truths:
    - "CsvExportService.BuildCsv produces a valid CSV string with the correct 9-column header and one data row per merged permission entry"
    - "Entries with identical Users + PermissionLevels + GrantedThrough but different URLs are merged into one row with pipe-joined URLs (Merge-PermissionRows port)"
    - "HtmlExportService.BuildHtml produces a self-contained HTML file (no external CSS/JS dependencies) that contains all user display names from the input"
    - "HTML report includes stats cards: Total Entries, Unique Permission Sets, Distinct Users/Groups"
    - "CSV fields with commas or quotes are correctly escaped per RFC 4180"
  artifacts:
    - path: "SharepointToolbox/Services/Export/CsvExportService.cs"
      provides: "Merges PermissionEntry rows and writes CSV"
      exports: ["CsvExportService"]
    - path: "SharepointToolbox/Services/Export/HtmlExportService.cs"
      provides: "Generates self-contained interactive HTML report"
      exports: ["HtmlExportService"]
  key_links:
    - from: "CsvExportService.cs"
      to: "PermissionEntry"
      via: "groups by (Users, PermissionLevels, GrantedThrough)"
      pattern: "GroupBy"
    - from: "HtmlExportService.cs"
      to: "PermissionEntry"
      via: "iterates all entries to build HTML rows"
      pattern: "foreach.*PermissionEntry"
---

<objective>
Implement the two export services: `CsvExportService` (port of PS `Merge-PermissionRows` + `Export-Csv`) and `HtmlExportService` (port of PS `Export-PermissionsToHTML`). Both services consume `IReadOnlyList<PermissionEntry>` and write files.

Purpose: Deliver PERM-05 (CSV export) and PERM-06 (HTML export). These are pure data-transformation services with no UI dependency — they can be verified fully by the automated test stubs created in Plan 01.
Output: 2 export service 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/phases/02-permissions/02-RESEARCH.md

<interfaces>
<!-- PermissionEntry defined in Plan 02 -->
From SharepointToolbox/Core/Models/PermissionEntry.cs:
```csharp
public record PermissionEntry(
    string ObjectType,          // "Site Collection" | "Site" | "List" | "Folder"
    string Title,
    string Url,
    bool   HasUniquePermissions,
    string Users,               // Semicolon-joined display names
    string UserLogins,          // Semicolon-joined login names
    string PermissionLevels,    // Semicolon-joined role names
    string GrantedThrough,      // "Direct Permissions" | "SharePoint Group: <name>"
    string PrincipalType        // "SharePointGroup" | "User" | "External User"
);
```

CSV merge logic (port of PS Merge-PermissionRows):
- Group by key: (Users, PermissionLevels, GrantedThrough)
- For each group: collect all Urls, join with " | "
- Collect all Titles, join with " | "
- Take first ObjectType, HasUniquePermissions from group

CSV columns (9 total): Object, Title, URL, HasUniquePermissions, Users, UserLogins, Type, Permissions, GrantedThrough
CSV escaping: enclose every field in double quotes, escape internal quotes by doubling them.

HTML report key features (port of PS Export-PermissionsToHTML):
- Stats cards: Total Entries (count of entries), Unique Permission Sets (count of distinct PermissionLevels values), Distinct Users/Groups (count of distinct users across all UserLogins)
- Filter input (vanilla JS filterTable())
- Type badge: color-coded span for ObjectType ("Site Collection"=blue, "Site"=green, "List"=yellow, "Folder"=gray)
- Unique vs Inherited badge per row (HasUniquePermissions → green "Unique", else gray "Inherited")
- User pills with data-email attribute for each login in UserLogins (split by ;)
- Self-contained: all CSS and JS inline in the HTML string — no external file dependencies
- Table columns: Object, Title, URL, Unique, Users, Permissions, Granted Through
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Implement CsvExportService</name>
  <files>
    SharepointToolbox/Services/Export/CsvExportService.cs
  </files>
  <behavior>
    - BuildCsv(IReadOnlyList&lt;PermissionEntry&gt; entries) returns string
    - Header row: Object,Title,URL,HasUniquePermissions,Users,UserLogins,Type,Permissions,GrantedThrough (all quoted)
    - Merge rows: entries grouped by (Users, PermissionLevels, GrantedThrough) → one output row per group with URLs pipe-joined
    - Fields with commas, double quotes, or newlines are wrapped in double quotes with internal quotes doubled
    - WriteAsync(entries, filePath, ct) calls BuildCsv then writes UTF-8 with BOM (for Excel compatibility)
    - The test from Plan 01 (BuildCsv_WithDuplicateUserPermissionGrantedThrough_MergesLocations) passes
  </behavior>
  <action>
    Create `SharepointToolbox/Services/Export/` directory if it doesn't exist.
    Create `SharepointToolbox/Services/Export/CsvExportService.cs`:

    ```csharp
    namespace SharepointToolbox.Services.Export;
    public class CsvExportService
    {
        private const string Header =
            "\"Object\",\"Title\",\"URL\",\"HasUniquePermissions\",\"Users\",\"UserLogins\",\"Type\",\"Permissions\",\"GrantedThrough\"";

        public string BuildCsv(IReadOnlyList<PermissionEntry> entries)
        {
            var sb = new StringBuilder();
            sb.AppendLine(Header);
            // Merge: group by (Users, PermissionLevels, GrantedThrough)
            var merged = entries
                .GroupBy(e => (e.Users, e.PermissionLevels, e.GrantedThrough))
                .Select(g => new
                {
                    ObjectType   = g.First().ObjectType,
                    Title        = string.Join(" | ", g.Select(e => e.Title).Distinct()),
                    Url          = string.Join(" | ", g.Select(e => e.Url).Distinct()),
                    HasUnique    = g.First().HasUniquePermissions,
                    Users        = g.Key.Users,
                    UserLogins   = g.First().UserLogins,
                    PrincipalType= g.First().PrincipalType,
                    Permissions  = g.Key.PermissionLevels,
                    GrantedThrough = g.Key.GrantedThrough
                });
            foreach (var row in merged)
                sb.AppendLine(string.Join(",", new[]
                {
                    Csv(row.ObjectType), Csv(row.Title), Csv(row.Url),
                    Csv(row.HasUnique.ToString()), Csv(row.Users), Csv(row.UserLogins),
                    Csv(row.PrincipalType), Csv(row.Permissions), Csv(row.GrantedThrough)
                }));
            return sb.ToString();
        }

        public async Task WriteAsync(IReadOnlyList<PermissionEntry> entries, string filePath, CancellationToken ct)
        {
            var csv = BuildCsv(entries);
            await File.WriteAllTextAsync(filePath, csv, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true), ct);
        }

        private static string Csv(string value)
        {
            if (string.IsNullOrEmpty(value)) return "\"\"";
            return $"\"{value.Replace("\"", "\"\"")}\"";
        }
    }
    ```

    Usings: `System.IO`, `System.Text`, `SharepointToolbox.Core.Models`.
    Namespace: `SharepointToolbox.Services.Export`.
  </action>
  <verify>
    <automated>dotnet test C:/Users/dev/Documents/projets/Sharepoint/SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "FullyQualifiedName~CsvExportServiceTests" -x</automated>
  </verify>
  <done>All 3 CsvExportServiceTests pass (header row present, merge works, empty list returns header only). dotnet build 0 errors.</done>
</task>

<task type="auto" tdd="true">
  <name>Task 2: Implement HtmlExportService</name>
  <files>
    SharepointToolbox/Services/Export/HtmlExportService.cs
  </files>
  <behavior>
    - BuildHtml(IReadOnlyList&lt;PermissionEntry&gt; entries) returns a self-contained HTML string
    - Output contains user display names from the input (test: BuildHtml_WithKnownEntries_ContainsUserNames passes)
    - Output contains all inline CSS and JS — no &lt;link&gt; or &lt;script src=...&gt; tags
    - Stats cards reflect: Total Entries count, Unique Permission Sets (distinct PermissionLevels values), Distinct Users (distinct entries in UserLogins split by semicolon)
    - Type badge CSS classes: site-coll, site, list, folder — color-coded
    - Unique/Inherited badge based on HasUniquePermissions
    - Filter input calls JS filterTable() on keyup — filters by any visible text in the row
    - External user tag: if UserLogins contains "#EXT#", user pill gets class "external-user" and data-email attribute
    - WriteAsync(entries, filePath, ct) writes UTF-8 (no BOM for HTML)
    - The test from Plan 01 (BuildHtml_WithExternalUser_ContainsExtHashMarker) passes — HTML contains "external-user" class
  </behavior>
  <action>
    Create `SharepointToolbox/Services/Export/HtmlExportService.cs`.

    Structure the HTML report as a multi-line C# string literal inside BuildHtml(). Use `StringBuilder` to assemble:
    1. HTML head (with inline CSS): table styles, badge styles (site-coll=blue, site=green, list=amber, folder=gray, unique=green, inherited=gray), user pill styles, external-user pill style (orange border), stats card styles, filter input style
    2. Body open: h1 "SharePoint Permissions Report", stats cards div (compute counts from entries), filter input
    3. Table with columns: Object | Title | URL | Unique | Users/Groups | Permission Level | Granted Through
    4. For each entry: one `<tr>` with:
       - `<td><span class="{objectTypeCss}">{ObjectType}</span></td>`
       - `<td>{Title}</td>`
       - `<td><a href="{Url}" target="_blank">Link</a></td>`
       - `<td><span class="{uniqueCss}">{Unique/Inherited}</span></td>`
       - `<td>` + user pills: split UserLogins by ';', split Users by ';', zip them, render `<span class="user-pill {externalClass}" data-email="{login}">{name}</span>`
       - `<td>{PermissionLevels}</td>`
       - `<td>{GrantedThrough}</td>`
    5. Inline JS: filterTable() function that iterates `<tr>` elements and shows/hides based on input text match against `tr.textContent`
    6. Close body/html

    Helper method `private static string ObjectTypeCss(string t)`:
    - "Site Collection" → "badge site-coll"
    - "Site" → "badge site"
    - "List" → "badge list"
    - "Folder" → "badge folder"
    - else → "badge"

    Stats computation:
    ```csharp
    var totalEntries = entries.Count;
    var uniquePermSets = entries.Select(e => e.PermissionLevels).Distinct().Count();
    var distinctUsers = entries.SelectMany(e => e.UserLogins.Split(';', StringSplitOptions.RemoveEmptyEntries))
        .Select(u => u.Trim()).Where(u => u.Length > 0).Distinct().Count();
    ```

    Namespace: `SharepointToolbox.Services.Export`.
    Usings: `System.IO`, `System.Text`, `SharepointToolbox.Core.Models`.
  </action>
  <verify>
    <automated>dotnet test C:/Users/dev/Documents/projets/Sharepoint/SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "FullyQualifiedName~HtmlExportServiceTests" -x</automated>
  </verify>
  <done>All 3 HtmlExportServiceTests pass (user name present, empty list produces valid HTML, external user gets external-user class). dotnet build 0 errors.</done>
</task>

</tasks>

<verification>
- `dotnet test C:/Users/dev/Documents/projets/Sharepoint/SharepointToolbox.slnx` → all tests pass (Phase 1 + new export tests)
- CsvExportServiceTests: 3 green
- HtmlExportServiceTests: 3 green
- HTML output contains no external script/link tags (grep verifiable: no `src=` or `href=` outside the table)
</verification>

<success_criteria>
- CsvExportService merges rows by (Users, PermissionLevels, GrantedThrough) before writing
- CSV uses UTF-8 with BOM for Excel compatibility
- HtmlExportService produces self-contained HTML with inline CSS and JS
- HTML correctly marks external users with "external-user" CSS class
- All 6 export tests pass (3 CSV + 3 HTML)
</success_criteria>

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