Source

Listings

Find code

Search source listings

.gitignore

# Visual Studio local state
.vs/
*.suo
*.user
*.userosscache
*.sln.docstates

# Build results
[Bb]in/
[Oo]bj/
[Dd]ebug/
[Rr]elease/
x86/
x64/

# Test results
TestResults/
*.trx
*.coverage
*.coveragexml

# NuGet
packages/
*.nupkg
project.assets.json
project.nuget.cache

# Logs and temporary files
*.log
*.tmp
*.temp
~$*

*.pdf

Dockerfile

# escape=`
FROM mcr.microsoft.com/dotnet/framework/sdk:4.8-windowsservercore-ltsc2022 AS build

SHELL ["cmd", "/S", "/C"]

WORKDIR C:\src

COPY ["AnimalRescueManager.slnx", "./"]
COPY ["AnimalRescueManager/", "./AnimalRescueManager/"]

RUN msbuild AnimalRescueManager\AnimalRescueManager.csproj /t:Build /p:Configuration=Release /p:Platform=AnyCPU

RUN mkdir C:\app && xcopy AnimalRescueManager\bin\Release C:\app\ /E /I /Y

WORKDIR C:\app

CMD ["cmd", "/S", "/C", "echo Build output is in C:\\app. Run AnimalRescueManager.exe interactively on Windows outside Docker. && dir C:\\app"]

AnimalRescueManager.slnx

<Solution>
  <Project Path="AnimalRescueManager/AnimalRescueManager.csproj" Id="3e71a153-f78b-46c5-a95c-d8bcf284f311" />
</Solution>

README.md

# Animal Rescue Manager

Animal Rescue Manager is a Windows desktop application for helping an animal rescue not-for-profit manage incoming and adopted animals. It was built for the the practical coding task.

The application uses a WPF user interface and flat CSV files for storage. Active animals are stored in `animals.csv`; archived/adopted animals are stored in `archives.csv`. These files are created automatically beside the running executable.

## Requirements

- Windows
- .NET Framework 4.8
- Visual Studio with the .NET desktop development workload, or Visual Studio MSBuild

This is a .NET Framework WPF project. Build it with Visual Studio/MSBuild rather than `dotnet build`.

## Build and Run

Open `AnimalRescueManager.slnx` in Visual Studio and build the solution.

To build from a Developer PowerShell or Developer Command Prompt:

```powershell
MSBuild.exe AnimalRescueManager.slnx /t:Build /p:Configuration=Release
```

After building, run:

```text
AnimalRescueManager\bin\Release\AnimalRescueManager.exe
```

Debug builds are written to:

```text
AnimalRescueManager\bin\Debug\AnimalRescueManager.exe
```

## How to Use

### Animals Tab

- View all active animals.
- Search active animals by name or species.
- Clear the search to show all active animals again.
- Sort active animals by species.
- Display the three oldest animals for each species.
- Remove an animal by selecting it and choosing `Remove selected`.
- Remove an animal by entering its 8-digit ID in `Remove by ID`.
- Archive an adopted animal by selecting the animal, choosing an adopted date, and selecting `Archive selected`.

### Add / Edit Tab

- Select `New` from the Animals tab to add a new animal.
- Select an active animal and choose `Edit selected` to update it.
- Enter the required animal details:
  - Species
  - Name
  - Gender
  - Spayed status
  - Breed
  - Colour
  - Birthday
  - Vaccine status
  - Identification status, type, and number when applicable
- Breed choices are validated based on species.
- Adoption fee is calculated automatically from birthday:
  - Animals below one year old: `$300`
  - Animals above ten years old: `$100`
  - All other animals: `$200`

### Archives Tab

- View archived/adopted animals.
- Search archived animals by archive date range.
- Restore a selected archived animal back to the active animal list.
- Archive all active animals with an adopted date at least three months old.

### Help Tab

The application includes a Help tab with quick usage instructions and data file notes.

## Data Format

The CSV files use this header:

```csv
ID,Species,Name,Gender,Spayed,Breed,Colour,Birthday,VaccineStatus,Identification,IdType,IdNumber,AdoptionFee,IsArchived,AdoptedDate,ArchiveDate
```

IDs are generated as incrementing 8-digit, zero-padded values when displayed and saved. Example:

```text
00000001
```

Dates are stored in `dd/MM/yyyy` format.

## Docker Build

A build-only Dockerfile is included for bonus/deployment evidence. Because this is a Windows WPF GUI application, Docker is not intended for normal interactive app use. Use the executable directly on Windows to operate the application.

Docker Desktop must be running in Windows container mode.

Build the image:

```powershell
docker build -t animal-rescue-manager-build .
```

Run the image to list the compiled Release output:

```powershell
docker run --rm animal-rescue-manager-build
```

The container builds the application with MSBuild and places compiled output in `C:\app`.

SCREENSHOTS.md

# Application Screenshots

These screenshots document the main Animal Rescue Manager workflows required for Part B deployment evidence.

## 01 - Application Opened

![Application opened on the Animals tab](screenshots/01-app-open.png)

The application opens on the Animals tab and displays the active animal list. The screen shows sample records with generated 8-digit IDs, animal details, search controls, sorting controls, remove controls, and the archive action.

## 02 - Add Animal

![Add animal form](screenshots/02-add-animal.png)

The Add / Edit tab is used to enter a new animal. This example shows a cat named Tom with species, gender, spayed status, breed, colour, birthday, vaccine status, and adopted date entered. The adoption fee is calculated automatically from the birthday.

## 03 - Animal Added

![Animal added to active list](screenshots/03-animal-added.png)

After saving, the new animal appears in the active animal list with the generated ID `00000004`. This confirms the create workflow and flat-file record display.

## 04 - Edit Animal

![Edit selected animal](screenshots/04-edit-animal.png)

Selecting an active animal and choosing `Edit selected` loads that animal into the Add / Edit tab. This example shows Happy with the existing fields populated for updating.

## 05 - Search by Species

![Search by species](screenshots/05-search-species.png)

The Animals tab supports searching active animals by name or species. This example searches for `Bird` and returns the matching bird record.

## 06 - Sort by Species

![Sort active animals by species](screenshots/06-sort-by-species.png)

The `Sort by species` action displays active animals ordered by species, while keeping the same animal details visible in the grid.

## 07 - Three Oldest by Species

![Three oldest animals by species](screenshots/07-three-oldest.png)

The `Three oldest` action displays the oldest animals for each species based on birthday. The sample data includes multiple dog records so the result demonstrates age-based ordering within a species.

## 08 - Remove by ID

![Remove animal by ID confirmation](screenshots/08-remove-by-id.png)

The remove-by-ID workflow accepts an animal ID and asks for confirmation before deleting the active animal. This example confirms removal of animal `00000005`.

## 09 - Archive Animal

![Archived animals list](screenshots/09-archive-animal.png)

The Archives tab shows adopted animals that have been archived. This example shows archived records with archive date filtering controls, adoption fees, and adopted dates.

## 10 - Restore Animal

![Restore archived animal](screenshots/10-restore-animal.png)

The restore workflow moves an archived animal back to the active animal list. This example shows the status message after restoring Kirk, leaving Tom in the archive list.

## 11 - Help

![Help tab usage instructions](screenshots/11-help.png)

The Help tab provides usage instructions for the Animals, Add / Edit, and Archives tabs. It also explains that active animals are stored in `animals.csv` and archived animals are stored in `archives.csv`.

AnimalRescueManager/App.config

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <startup> 
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
    </startup>
</configuration>

AnimalRescueManager/App.xaml

<Application x:Class="AnimalRescueManager.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:AnimalRescueManager"
             StartupUri="Views/MainPage.xaml">
    <Application.Resources>
         
    </Application.Resources>
</Application>

AnimalRescueManager/App.xaml.cs

using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;

namespace AnimalRescueManager
{
    /// <summary>
    /// Interaction logic for App.xaml
    /// </summary>
    public partial class App : Application
    {
    }
}

AnimalRescueManager/AnimalRescueManager.csproj

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
  <PropertyGroup>
    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
    <ProjectGuid>{3E71A153-F78B-46C5-A95C-D8BCF284F311}</ProjectGuid>
    <OutputType>WinExe</OutputType>
    <RootNamespace>AnimalRescueManager</RootNamespace>
    <AssemblyName>AnimalRescueManager</AssemblyName>
    <TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
    <FileAlignment>512</FileAlignment>
    <ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
    <WarningLevel>4</WarningLevel>
    <AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
    <Deterministic>true</Deterministic>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
    <PlatformTarget>AnyCPU</PlatformTarget>
    <DebugSymbols>true</DebugSymbols>
    <DebugType>full</DebugType>
    <Optimize>false</Optimize>
    <OutputPath>bin\Debug\</OutputPath>
    <DefineConstants>DEBUG;TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
    <PlatformTarget>AnyCPU</PlatformTarget>
    <DebugType>pdbonly</DebugType>
    <Optimize>true</Optimize>
    <OutputPath>bin\Release\</OutputPath>
    <DefineConstants>TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <ItemGroup>
    <Reference Include="System" />
    <Reference Include="System.ComponentModel.DataAnnotations" />
    <Reference Include="System.Data" />
    <Reference Include="System.Xml" />
    <Reference Include="Microsoft.CSharp" />
    <Reference Include="System.Core" />
    <Reference Include="System.Xml.Linq" />
    <Reference Include="System.Data.DataSetExtensions" />
    <Reference Include="System.Net.Http" />
    <Reference Include="System.Xaml">
      <RequiredTargetFramework>4.0</RequiredTargetFramework>
    </Reference>
    <Reference Include="WindowsBase" />
    <Reference Include="PresentationCore" />
    <Reference Include="PresentationFramework" />
  </ItemGroup>
  <ItemGroup>
    <ApplicationDefinition Include="App.xaml">
      <Generator>MSBuild:Compile</Generator>
      <SubType>Designer</SubType>
    </ApplicationDefinition>
    <Page Include="Views\MainPage.xaml">
      <Generator>MSBuild:Compile</Generator>
      <SubType>Designer</SubType>
    </Page>
    <Compile Include="App.xaml.cs">
      <DependentUpon>App.xaml</DependentUpon>
      <SubType>Code</SubType>
    </Compile>
    <Compile Include="Services\DataService.cs" />
    <Compile Include="Views\MainPage.xaml.cs">
      <DependentUpon>MainPage.xaml</DependentUpon>
      <SubType>Code</SubType>
    </Compile>
  </ItemGroup>
  <ItemGroup>
    <Compile Include="Models\Animal.cs" />
    <Compile Include="Models\Enums.cs" />
    <Compile Include="Properties\AssemblyInfo.cs">
      <SubType>Code</SubType>
    </Compile>
    <Compile Include="Properties\Resources.Designer.cs">
      <AutoGen>True</AutoGen>
      <DesignTime>True</DesignTime>
      <DependentUpon>Resources.resx</DependentUpon>
    </Compile>
    <Compile Include="Properties\Settings.Designer.cs">
      <AutoGen>True</AutoGen>
      <DependentUpon>Settings.settings</DependentUpon>
      <DesignTimeSharedInput>True</DesignTimeSharedInput>
    </Compile>
    <EmbeddedResource Include="Properties\Resources.resx">
      <Generator>ResXFileCodeGenerator</Generator>
      <LastGenOutput>Resources.Designer.cs</LastGenOutput>
    </EmbeddedResource>
    <None Include="Properties\Settings.settings">
      <Generator>SettingsSingleFileGenerator</Generator>
      <LastGenOutput>Settings.Designer.cs</LastGenOutput>
    </None>
  </ItemGroup>
  <ItemGroup>
    <None Include="App.config" />
  </ItemGroup>
  <ItemGroup />
  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

AnimalRescueManager/Models/Animal.cs

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace AnimalRescueManager.Models
{
    /// <summary>
    /// Represents one animal record stored by the rescue manager.
    /// </summary>
    public class Animal
    {
        /// <summary>
        /// Auto-generated shelter identifier displayed as an 8-digit value.
        /// </summary>
        [Key]
        public int Id { get; set; }

        /// <summary>
        /// Main species category used for searching, sorting, and breed validation.
        /// </summary>
        [Required]
        public Species Species { get; set; }

        /// <summary>
        /// Animal's display name.
        /// </summary>
        [Required]
        public string Name { get; set; }

        /// <summary>
        /// Animal's recorded gender.
        /// </summary>
        [Required]
        public Gender Gender { get; set; }

        /// <summary>
        /// Indicates whether the animal has been spayed or neutered.
        /// </summary>
        [Required]
        public bool Spayed { get; set; }

        /// <summary>
        /// Breed value selected from the allowed list for the animal's species.
        /// </summary>
        [Required]
        public string Breed { get; set; }

        /// <summary>
        /// Primary colour or marking description.
        /// </summary>
        [Required]
        public string Colour { get; set; }

        /// <summary>
        /// Approximate birth date used to calculate the adoption fee.
        /// </summary>
        [Required]
        [DisplayFormat(DataFormatString = "{0:dd/MM/yyyy}", ApplyFormatInEditMode = true)]
        public DateTime Birthday { get; set; }

        /// <summary>
        /// Current vaccine status for the animal.
        /// </summary>
        [Required]
        public VaccineStatus VaccineStatus { get; set; }

        /// <summary>
        /// Indicates whether an identification tag or chip is recorded.
        /// </summary>
        [Required]
        public bool Identification { get; set; }

        /// <summary>
        /// Identification method; required only when Identification is true.
        /// </summary>
        public IdentificationType? IdType { get; set; }

        /// <summary>
        /// Identification tag, chip, or barcode number.
        /// </summary>
        public string IdNumber { get; set; }

        /// <summary>
        /// Fee calculated from the animal's age, capped by the assignment rules.
        /// </summary>
        [Required]
        [Range(0, 300)]
        public decimal AdoptionFee { get; set; }

        /// <summary>
        /// Separates active animals from adopted animals stored in the archive file.
        /// </summary>
        public bool IsArchived { get; set; }

        /// <summary>
        /// Date the animal was adopted, when known.
        /// </summary>
        [DisplayFormat(DataFormatString = "{0:dd/MM/yyyy}", ApplyFormatInEditMode = true)]
        public DateTime? AdoptedDate { get; set; }

        /// <summary>
        /// Date the animal was moved into the archive file.
        /// </summary>
        [DisplayFormat(DataFormatString = "{0:dd/MM/yyyy}", ApplyFormatInEditMode = true)]
        public DateTime? ArchiveDate { get; set; }
    }
}

AnimalRescueManager/Models/Enums.cs

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace AnimalRescueManager.Models
{
    /// <summary>
    /// Gender values displayed and saved for each animal.
    /// </summary>
    public enum Gender
    {
        /// <summary>
        /// Male.
        /// </summary>
        M,

        /// <summary>
        /// Female.
        /// </summary>
        F
    }

    /// <summary>
    /// Species categories required by the Part B animal rescue task.
    /// </summary>
    public enum Species
    {
        /// <summary>
        /// Dog.
        /// </summary>
        Dog,

        /// <summary>
        /// Cat.
        /// </summary>
        Cat,

        /// <summary>
        /// Bird.
        /// </summary>
        Bird,

        /// <summary>
        /// Rabbit.
        /// </summary>
        Rabbit,

        /// <summary>
        /// Small animals such as hamsters, guinea pigs, and rats.
        /// </summary>
        [Display(Name = "Small & Furry")]
        SmallAndFurry,

        /// <summary>
        /// Fish.
        /// </summary>
        Fish,

        /// <summary>
        /// Barnyard animals.
        /// </summary>
        Barnyard,

        /// <summary>
        /// Any animal outside the listed categories.
        /// </summary>
        Other
    }

    /// <summary>
    /// Vaccine statuses allowed by the assignment.
    /// </summary>
    public enum VaccineStatus
    {
        /// <summary>
        /// Vaccines are current.
        /// </summary>
        [Display(Name = "Up to date")]
        UpToDate,

        /// <summary>
        /// Vaccines are overdue.
        /// </summary>
        Late,

        /// <summary>
        /// Vaccine status is not known.
        /// </summary>
        Unknown
    }

    /// <summary>
    /// Identification methods available when an animal has recorded identification.
    /// </summary>
    public enum IdentificationType
    {
        /// <summary>
        /// Barcode tag identification.
        /// </summary>
        [Display(Name = "Bar code")]
        BarCode,

        /// <summary>
        /// Microchip identification.
        /// </summary>
        [Display(Name = "Micro-chipped")]
        MicroChipped
    }
}

AnimalRescueManager/Properties/AssemblyInfo.cs

using System.Reflection;
using System.Resources;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Windows;

// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("AnimalRescueManager")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("AnimalRescueManager")]
[assembly: AssemblyCopyright("Copyright ©  2026")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]

// Setting ComVisible to false makes the types in this assembly not visible
// to COM components.  If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]

//In order to begin building localizable applications, set
//<UICulture>CultureYouAreCodingWith</UICulture> in your .csproj file
//inside a <PropertyGroup>.  For example, if you are using US english
//in your source files, set the <UICulture> to en-US.  Then uncomment
//the NeutralResourceLanguage attribute below.  Update the "en-US" in
//the line below to match the UICulture setting in the project file.

//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]


[assembly: ThemeInfo(
    ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
                                     //(used if a resource is not found in the page,
                                     // or application resource dictionaries)
    ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
                                              //(used if a resource is not found in the page,
                                              // app, or any theme specific resource dictionaries)
)]


// Version information for an assembly consists of the following four values:
//
//      Major Version
//      Minor Version
//      Build Number
//      Revision
//
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

AnimalRescueManager/Properties/Resources.Designer.cs

//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by a tool.
//     Runtime Version:4.0.30319.42000
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

namespace AnimalRescueManager.Properties
{


    /// <summary>
    ///   A strongly-typed resource class, for looking up localized strings, etc.
    /// </summary>
    // This class was auto-generated by the StronglyTypedResourceBuilder
    // class via a tool like ResGen or Visual Studio.
    // To add or remove a member, edit your .ResX file then rerun ResGen
    // with the /str option, or rebuild your VS project.
    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
    [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
    [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
    internal class Resources
    {

        private static global::System.Resources.ResourceManager resourceMan;

        private static global::System.Globalization.CultureInfo resourceCulture;

        [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
        internal Resources()
        {
        }

        /// <summary>
        ///   Returns the cached ResourceManager instance used by this class.
        /// </summary>
        [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
        internal static global::System.Resources.ResourceManager ResourceManager
        {
            get
            {
                if ((resourceMan == null))
                {
                    global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("AnimalRescueManager.Properties.Resources", typeof(Resources).Assembly);
                    resourceMan = temp;
                }
                return resourceMan;
            }
        }

        /// <summary>
        ///   Overrides the current thread's CurrentUICulture property for all
        ///   resource lookups using this strongly typed resource class.
        /// </summary>
        [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
        internal static global::System.Globalization.CultureInfo Culture
        {
            get
            {
                return resourceCulture;
            }
            set
            {
                resourceCulture = value;
            }
        }
    }
}

AnimalRescueManager/Properties/Resources.resx

<?xml version="1.0" encoding="utf-8"?>
<root>
  <!-- 
    Microsoft ResX Schema 
    
    Version 2.0
    
    The primary goals of this format is to allow a simple XML format 
    that is mostly human readable. The generation and parsing of the 
    various data types are done through the TypeConverter classes 
    associated with the data types.
    
    Example:
    
    ... ado.net/XML headers & schema ...
    <resheader name="resmimetype">text/microsoft-resx</resheader>
    <resheader name="version">2.0</resheader>
    <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
    <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
    <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
    <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
    <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
        <value>[base64 mime encoded serialized .NET Framework object]</value>
    </data>
    <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
        <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
        <comment>This is a comment</comment>
    </data>
                
    There are any number of "resheader" rows that contain simple 
    name/value pairs.
    
    Each data row contains a name, and value. The row also contains a 
    type or mimetype. Type corresponds to a .NET class that support 
    text/value conversion through the TypeConverter architecture. 
    Classes that don't support this are serialized and stored with the 
    mimetype set.
    
    The mimetype is used for serialized objects, and tells the 
    ResXResourceReader how to depersist the object. This is currently not 
    extensible. For a given mimetype the value must be set accordingly:
    
    Note - application/x-microsoft.net.object.binary.base64 is the format 
    that the ResXResourceWriter will generate, however the reader can 
    read any of the formats listed below.
    
    mimetype: application/x-microsoft.net.object.binary.base64
    value   : The object must be serialized with 
            : System.Serialization.Formatters.Binary.BinaryFormatter
            : and then encoded with base64 encoding.
    
    mimetype: application/x-microsoft.net.object.soap.base64
    value   : The object must be serialized with 
            : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
            : and then encoded with base64 encoding.

    mimetype: application/x-microsoft.net.object.bytearray.base64
    value   : The object must be serialized into a byte array 
            : using a System.ComponentModel.TypeConverter
            : and then encoded with base64 encoding.
    -->
  <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
    <xsd:element name="root" msdata:IsDataSet="true">
      <xsd:complexType>
        <xsd:choice maxOccurs="unbounded">
          <xsd:element name="metadata">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" />
              </xsd:sequence>
              <xsd:attribute name="name" type="xsd:string" />
              <xsd:attribute name="type" type="xsd:string" />
              <xsd:attribute name="mimetype" type="xsd:string" />
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="assembly">
            <xsd:complexType>
              <xsd:attribute name="alias" type="xsd:string" />
              <xsd:attribute name="name" type="xsd:string" />
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="data">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
                <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
              </xsd:sequence>
              <xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
              <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
              <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="resheader">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
              </xsd:sequence>
              <xsd:attribute name="name" type="xsd:string" use="required" />
            </xsd:complexType>
          </xsd:element>
        </xsd:choice>
      </xsd:complexType>
    </xsd:element>
  </xsd:schema>
  <resheader name="resmimetype">
    <value>text/microsoft-resx</value>
  </resheader>
  <resheader name="version">
    <value>2.0</value>
  </resheader>
  <resheader name="reader">
    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
  <resheader name="writer">
    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
</root>

AnimalRescueManager/Properties/Settings.Designer.cs

//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by a tool.
//     Runtime Version:4.0.30319.42000
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

namespace AnimalRescueManager.Properties
{


    [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")]
    internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase
    {

        private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));

        public static Settings Default
        {
            get
            {
                return defaultInstance;
            }
        }
    }
}

AnimalRescueManager/Properties/Settings.settings

<?xml version='1.0' encoding='utf-8'?>
<SettingsFile xmlns="uri:settings" CurrentProfile="(Default)">
  <Profiles>
    <Profile Name="(Default)" />
  </Profiles>
  <Settings />
</SettingsFile>

AnimalRescueManager/Services/DataService.cs

using AnimalRescueManager.Models;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;

namespace AnimalRescueManager.Services
{
    /// <summary>
    /// Handles validation, CSV storage, searching, sorting, and archiving for animal records.
    /// </summary>
    public class DataService
    {
        private const string DateFormat = "dd/MM/yyyy";

        // Active and archived files share the same header so one parser can read both files.
        private const string Header = "ID,Species,Name,Gender,Spayed,Breed,Colour,Birthday,VaccineStatus,Identification,IdType,IdNumber,AdoptionFee,IsArchived,AdoptedDate,ArchiveDate";

        // Breed options are centralized so the UI dropdown and save-time validation always match.
        private static readonly Dictionary<Species, string[]> ValidBreedOptions = new Dictionary<Species, string[]>
        {
            { Species.Dog, new[] { "Collie", "Beagle", "Labrador Retriever", "German Shepherd", "Poodle", "Bulldog", "Unknown", "Other" } },
            { Species.Cat, new[] { "Siamese", "Calico", "Tabby", "Persian", "Maine Coon", "Domestic Shorthair", "Unknown", "Other" } },
            { Species.Bird, new[] { "Parakeet", "Cockatiel", "Canary", "Parrot", "Finch", "Unknown", "Other" } },
            { Species.Rabbit, new[] { "Holland Lop", "Dutch", "Mini Rex", "Lionhead", "Unknown", "Other" } },
            { Species.SmallAndFurry, new[] { "Hamster", "Guinea Pig", "Gerbil", "Mouse", "Rat", "Chinchilla", "Unknown", "Other" } },
            { Species.Fish, new[] { "Goldfish", "Betta", "Guppy", "Tetra", "Unknown", "Other" } },
            { Species.Barnyard, new[] { "Goat", "Chicken", "Duck", "Sheep", "Pig", "Horse", "Unknown", "Other" } },
            { Species.Other, new[] { "Unknown", "Other" } }
        };

        private readonly string _filePath;
        private readonly string _archivePath;

        /// <summary>
        /// Creates a service that stores active and archived animals in the default CSV files.
        /// </summary>
        public DataService()
            : this("animals.csv", "archives.csv")
        {
        }

        /// <summary>
        /// Creates a service for the supplied active and archive CSV paths.
        /// </summary>
        public DataService(string filePath, string archivePath)
        {
            if (string.IsNullOrWhiteSpace(filePath))
            {
                throw new ArgumentException("Animal file path is required.", nameof(filePath));
            }

            if (string.IsNullOrWhiteSpace(archivePath))
            {
                throw new ArgumentException("Archive file path is required.", nameof(archivePath));
            }

            _filePath = filePath;
            _archivePath = archivePath;

            InitializeFile(_filePath);
            InitializeFile(_archivePath);
        }

        /// <summary>
        /// Returns all non-archived animals from the active animal file.
        /// </summary>
        public List<Animal> GetAllAnimals()
        {
            return LoadAnimals(_filePath, false)
                .Where(a => !a.IsArchived)
                .ToList();
        }

        /// <summary>
        /// Returns active animals, optionally including archived records.
        /// </summary>
        public List<Animal> GetAllAnimals(bool includeArchived)
        {
            var animals = GetAllAnimals();

            if (includeArchived)
            {
                animals.AddRange(GetArchivedAnimals());
            }

            return animals;
        }

        /// <summary>
        /// Returns all animals that have been moved to the archive file.
        /// </summary>
        public List<Animal> GetArchivedAnimals()
        {
            return LoadAnimals(_archivePath, true)
                .Where(a => a.IsArchived)
                .ToList();
        }

        /// <summary>
        /// Validates and saves a new active animal with the next available 8-digit ID.
        /// </summary>
        public void AddAnimal(Animal animal)
        {
            if (animal == null)
            {
                throw new ArgumentNullException(nameof(animal));
            }

            var activeAnimals = GetAllAnimals();
            var archivedAnimals = GetArchivedAnimals();

            animal.Id = GetNextId(activeAnimals, archivedAnimals);
            animal.IsArchived = false;
            animal.ArchiveDate = null;
            animal.AdoptionFee = CalculateAdoptionFee(animal.Birthday);

            ValidateAnimal(animal);
            activeAnimals.Add(animal);
            SaveAll(activeAnimals, _filePath);
        }

        /// <summary>
        /// Replaces an existing active animal while preserving active-file storage rules.
        /// </summary>
        public void UpdateAnimal(Animal animal)
        {
            if (animal == null)
            {
                throw new ArgumentNullException(nameof(animal));
            }

            if (animal.Id <= 0)
            {
                throw new ValidationException("Animal ID is required for update.");
            }

            var activeAnimals = GetAllAnimals();
            int index = activeAnimals.FindIndex(a => a.Id == animal.Id);

            if (index < 0)
            {
                throw new KeyNotFoundException("No active animal exists with ID " + animal.Id.ToString("D8", CultureInfo.InvariantCulture) + ".");
            }

            animal.IsArchived = false;
            animal.ArchiveDate = null;
            animal.AdoptionFee = CalculateAdoptionFee(animal.Birthday);

            ValidateAnimal(animal);
            activeAnimals[index] = animal;
            SaveAll(activeAnimals, _filePath);
        }

        /// <summary>
        /// Removes an active animal by ID without adding it to the archive.
        /// </summary>
        public void RemoveAnimal(int id)
        {
            if (id <= 0)
            {
                throw new ValidationException("Animal ID is required for removal.");
            }

            var activeAnimals = GetAllAnimals();
            int removed = activeAnimals.RemoveAll(a => a.Id == id);

            if (removed == 0)
            {
                throw new KeyNotFoundException("No active animal exists with ID " + id.ToString("D8", CultureInfo.InvariantCulture) + ".");
            }

            SaveAll(activeAnimals, _filePath);
        }

        /// <summary>
        /// Searches active animals by name, enum name, or display value for species.
        /// </summary>
        public List<Animal> SearchAnimals(string query)
        {
            var animals = GetAllAnimals();

            if (string.IsNullOrWhiteSpace(query))
            {
                return animals;
            }

            string normalizedQuery = query.Trim();

            return animals
                .Where(a => ContainsIgnoreCase(a.Name, normalizedQuery)
                    || ContainsIgnoreCase(a.Species.ToString(), normalizedQuery)
                    || ContainsIgnoreCase(GetEnumCsvValue(a.Species), normalizedQuery))
                .ToList();
        }

        /// <summary>
        /// Returns active animals ordered by species and then name.
        /// </summary>
        public List<Animal> GetAnimalsSortedBySpecies()
        {
            return GetAllAnimals()
                .OrderBy(a => a.Species)
                .ThenBy(a => a.Name)
                .ToList();
        }

        /// <summary>
        /// Returns up to three oldest active animals from each species category.
        /// </summary>
        public List<Animal> GetThreeOldestBySpecies()
        {
            return GetAllAnimals()
                .GroupBy(a => a.Species)
                .SelectMany(group => group.OrderBy(a => a.Birthday).Take(3))
                .OrderBy(a => a.Species)
                .ThenBy(a => a.Birthday)
                .ToList();
        }

        /// <summary>
        /// Moves an active animal into the archive with the supplied adoption date.
        /// </summary>
        public void ArchiveAnimal(int id, DateTime adoptedDate)
        {
            if (id <= 0)
            {
                throw new ValidationException("Animal ID is required for archive.");
            }

            if (adoptedDate == DateTime.MinValue)
            {
                throw new ValidationException("Adopted date is required for archive.");
            }

            var activeAnimals = GetAllAnimals();
            int index = activeAnimals.FindIndex(a => a.Id == id);

            if (index < 0)
            {
                throw new KeyNotFoundException("No active animal exists with ID " + id.ToString("D8", CultureInfo.InvariantCulture) + ".");
            }

            var animal = activeAnimals[index];

            // Keep active and archived files mutually exclusive by moving the record between lists.
            activeAnimals.RemoveAt(index);

            animal.IsArchived = true;
            animal.AdoptedDate = adoptedDate.Date;
            animal.ArchiveDate = DateTime.Today;
            animal.AdoptionFee = CalculateAdoptionFee(animal.Birthday);
            ValidateAnimal(animal);

            var archivedAnimals = GetArchivedAnimals();
            archivedAnimals.Add(animal);

            SaveAll(activeAnimals, _filePath);
            SaveAll(archivedAnimals, _archivePath);
        }

        /// <summary>
        /// Moves an archived animal back to the active animal file.
        /// </summary>
        public void RestoreAnimal(int id)
        {
            if (id <= 0)
            {
                throw new ValidationException("Animal ID is required for restore.");
            }

            var archivedAnimals = GetArchivedAnimals();
            int index = archivedAnimals.FindIndex(a => a.Id == id);

            if (index < 0)
            {
                throw new KeyNotFoundException("No archived animal exists with ID " + id.ToString("D8", CultureInfo.InvariantCulture) + ".");
            }

            var animal = archivedAnimals[index];

            // Restored animals are treated like active records again, so archive dates are cleared.
            archivedAnimals.RemoveAt(index);

            animal.IsArchived = false;
            animal.AdoptedDate = null;
            animal.ArchiveDate = null;
            animal.AdoptionFee = CalculateAdoptionFee(animal.Birthday);
            ValidateAnimal(animal);

            var activeAnimals = GetAllAnimals();
            activeAnimals.Add(animal);

            SaveAll(activeAnimals, _filePath);
            SaveAll(archivedAnimals, _archivePath);
        }

        /// <summary>
        /// Finds archived animals whose archive dates fall within the supplied range.
        /// </summary>
        public List<Animal> SearchArchivedAnimals(DateTime startDate, DateTime endDate)
        {
            if (startDate.Date > endDate.Date)
            {
                throw new ArgumentException("Start date must be on or before end date.");
            }

            return GetArchivedAnimals()
                .Where(a => a.ArchiveDate.HasValue
                    && a.ArchiveDate.Value.Date >= startDate.Date
                    && a.ArchiveDate.Value.Date <= endDate.Date)
                .ToList();
        }

        /// <summary>
        /// Archives all active animals adopted on or before the cutoff date.
        /// </summary>
        public int ArchiveAnimalsAdoptedBefore(DateTime cutoffDate)
        {
            var activeAnimals = GetAllAnimals();
            var animalsToArchive = activeAnimals
                .Where(a => a.AdoptedDate.HasValue && a.AdoptedDate.Value.Date <= cutoffDate.Date)
                .ToList();

            if (animalsToArchive.Count == 0)
            {
                return 0;
            }

            // Each selected animal is converted to an archived record before both CSV files are rewritten.
            foreach (var animal in animalsToArchive)
            {
                animal.IsArchived = true;
                animal.ArchiveDate = DateTime.Today;
                animal.AdoptionFee = CalculateAdoptionFee(animal.Birthday);
                ValidateAnimal(animal);
            }

            var remainingAnimals = activeAnimals
                .Where(a => !animalsToArchive.Any(archived => archived.Id == a.Id))
                .ToList();

            var archivedAnimals = GetArchivedAnimals();
            archivedAnimals.AddRange(animalsToArchive);

            SaveAll(remainingAnimals, _filePath);
            SaveAll(archivedAnimals, _archivePath);

            return animalsToArchive.Count;
        }

        /// <summary>
        /// Archives active animals whose adoption dates are at least three months old.
        /// </summary>
        public int ArchiveAnimalsAdoptedAtLeastThreeMonthsAgo()
        {
            return ArchiveAnimalsAdoptedBefore(DateTime.Today.AddMonths(-3));
        }

        /// <summary>
        /// Calculates the adoption fee from the assignment's age bands.
        /// </summary>
        public decimal CalculateAdoptionFee(DateTime birthday)
        {
            DateTime birthdayDate = birthday.Date;

            // Young animals cost more, senior animals cost less, and all others use the standard fee.
            if (birthdayDate > DateTime.Today.AddYears(-1))
            {
                return 300m;
            }

            if (birthdayDate < DateTime.Today.AddYears(-10))
            {
                return 100m;
            }

            return 200m;
        }

        /// <summary>
        /// Returns the breed dropdown values that are valid for a species.
        /// </summary>
        public List<string> GetValidBreeds(Species species)
        {
            if (!Enum.IsDefined(typeof(Species), species))
            {
                throw new ValidationException("Species is invalid.");
            }

            return ValidBreedOptions[species].ToList();
        }

        /// <summary>
        /// Normalizes a breed value to the canonical spelling used by the selected species.
        /// </summary>
        public string NormalizeBreed(Species species, string breed)
        {
            return NormalizeBreedValue(species, breed);
        }

        private void InitializeFile(string path)
        {
            string fullPath = Path.GetFullPath(path);
            string directory = Path.GetDirectoryName(fullPath);

            // Create first-run storage with the expected CSV header before any read or write.
            if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
            {
                Directory.CreateDirectory(directory);
            }

            if (!File.Exists(path) || new FileInfo(path).Length == 0)
            {
                File.WriteAllText(path, Header + Environment.NewLine);
            }
        }

        private List<Animal> LoadAnimals(string path, bool archivedFile)
        {
            InitializeFile(path);

            var animals = new List<Animal>();
            var lines = File.ReadAllLines(path);

            // Skip line 0 because it is the shared CSV header.
            for (int i = 1; i < lines.Length; i++)
            {
                string line = lines[i];

                if (string.IsNullOrWhiteSpace(line))
                {
                    continue;
                }

                var parts = ParseCsvLine(line, path, i + 1);
                animals.Add(ParseAnimal(parts, path, i + 1, archivedFile));
            }

            return animals;
        }

        private Animal ParseAnimal(List<string> parts, string path, int lineNumber, bool archivedFile)
        {
            // Older files may have fewer archive fields, so support the earlier and current layouts.
            if (parts.Count != 13 && parts.Count != 15 && parts.Count != 16)
            {
                throw new InvalidDataException("Invalid animal record in '" + path + "' at line " + lineNumber + ": expected 13, 15, or 16 fields but found " + parts.Count + ".");
            }

            try
            {
                bool isArchived = archivedFile;
                DateTime? adoptedDate = null;
                DateTime? archiveDate = null;

                if (parts.Count >= 14)
                {
                    // Records loaded from the archive file are archived even if the saved flag is blank.
                    isArchived = archivedFile || ParseOptionalBoolean(parts[13], false, "IsArchived");
                }

                if (parts.Count == 15)
                {
                    // Compatibility with an earlier archive layout that stored only ArchiveDate.
                    archiveDate = ParseOptionalDate(parts[14], "ArchiveDate");
                }
                else if (parts.Count == 16)
                {
                    adoptedDate = ParseOptionalDate(parts[14], "AdoptedDate");
                    archiveDate = ParseOptionalDate(parts[15], "ArchiveDate");
                }

                return new Animal
                {
                    Id = int.Parse(parts[0], NumberStyles.Integer, CultureInfo.InvariantCulture),
                    Species = ParseEnumCsvValue<Species>(parts[1], "Species"),
                    Name = parts[2],
                    Gender = ParseEnumCsvValue<Gender>(parts[3], "Gender"),
                    Spayed = ParseYesNo(parts[4], "Spayed"),
                    Breed = parts[5],
                    Colour = parts[6],
                    Birthday = DateTime.ParseExact(parts[7], DateFormat, CultureInfo.InvariantCulture),
                    VaccineStatus = ParseEnumCsvValue<VaccineStatus>(parts[8], "VaccineStatus"),
                    Identification = ParseYesNo(parts[9], "Identification"),
                    IdType = string.IsNullOrWhiteSpace(parts[10]) ? (IdentificationType?)null : ParseEnumCsvValue<IdentificationType>(parts[10], "IdType"),
                    IdNumber = parts[11],
                    AdoptionFee = decimal.Parse(parts[12], NumberStyles.Number, CultureInfo.InvariantCulture),
                    IsArchived = isArchived,
                    AdoptedDate = adoptedDate,
                    ArchiveDate = archiveDate
                };
            }
            catch (Exception ex) when (ex is ArgumentException || ex is FormatException || ex is OverflowException)
            {
                throw new InvalidDataException("Invalid animal record in '" + path + "' at line " + lineNumber + ": " + ex.Message, ex);
            }
        }

        private void SaveAll(List<Animal> animals, string path)
        {
            // Rebuild the file from validated records to avoid stale or partially updated rows.
            var lines = new List<string> { Header };

            foreach (var animal in animals.OrderBy(a => a.Id))
            {
                ValidateAnimal(animal);
                lines.Add(FormatAnimalToCsv(animal));
            }

            File.WriteAllLines(path, lines);
        }

        private string FormatAnimalToCsv(Animal animal)
        {
            return FormatCsvLine(new[]
            {
                animal.Id.ToString("D8", CultureInfo.InvariantCulture),
                GetEnumCsvValue(animal.Species),
                animal.Name,
                GetEnumCsvValue(animal.Gender),
                FormatYesNo(animal.Spayed),
                animal.Breed,
                animal.Colour,
                animal.Birthday.ToString(DateFormat, CultureInfo.InvariantCulture),
                GetEnumCsvValue(animal.VaccineStatus),
                FormatYesNo(animal.Identification),
                animal.IdType.HasValue ? GetEnumCsvValue(animal.IdType.Value) : string.Empty,
                animal.IdNumber ?? string.Empty,
                animal.AdoptionFee.ToString("0.##", CultureInfo.InvariantCulture),
                FormatYesNo(animal.IsArchived),
                FormatOptionalDate(animal.AdoptedDate),
                FormatOptionalDate(animal.ArchiveDate)
            });
        }

        private static void ValidateAnimal(Animal animal)
        {
            if (animal == null)
            {
                throw new ArgumentNullException(nameof(animal));
            }

            if (animal.Id < 0)
            {
                throw new ValidationException("Animal ID cannot be negative.");
            }

            if (!Enum.IsDefined(typeof(Species), animal.Species))
            {
                throw new ValidationException("Species is invalid.");
            }

            if (!Enum.IsDefined(typeof(Gender), animal.Gender))
            {
                throw new ValidationException("Gender is invalid.");
            }

            if (!Enum.IsDefined(typeof(VaccineStatus), animal.VaccineStatus))
            {
                throw new ValidationException("Vaccine status is invalid.");
            }

            if (animal.Birthday == DateTime.MinValue)
            {
                throw new ValidationException("Birthday is required.");
            }

            animal.Name = NormalizeRequiredText(animal.Name, "Name");
            animal.Breed = NormalizeBreedValue(animal.Species, animal.Breed);
            animal.Colour = NormalizeRequiredText(animal.Colour, "Colour");

            if (animal.Identification)
            {
                if (!animal.IdType.HasValue)
                {
                    throw new ValidationException("Identification type is required when identification is marked Yes.");
                }

                if (!Enum.IsDefined(typeof(IdentificationType), animal.IdType.Value))
                {
                    throw new ValidationException("Identification type is invalid.");
                }

                animal.IdNumber = NormalizeRequiredText(animal.IdNumber, "Identification number");
            }
            else
            {
                animal.IdType = null;
                animal.IdNumber = string.Empty;
            }
        }

        private static string NormalizeRequiredText(string value, string fieldName)
        {
            if (string.IsNullOrWhiteSpace(value))
            {
                throw new ValidationException(fieldName + " is required.");
            }

            string normalizedValue = value.Trim();

            if (ContainsNewline(normalizedValue))
            {
                throw new ValidationException(fieldName + " cannot contain line breaks.");
            }

            return normalizedValue;
        }

        private static string NormalizeBreedValue(Species species, string breed)
        {
            string normalizedBreed = NormalizeRequiredText(breed, "Breed");
            string[] validBreeds;

            if (!ValidBreedOptions.TryGetValue(species, out validBreeds))
            {
                throw new ValidationException("Species is invalid.");
            }

            string canonicalBreed = validBreeds
                .FirstOrDefault(value => string.Equals(value, normalizedBreed, StringComparison.OrdinalIgnoreCase));

            if (canonicalBreed == null)
            {
                throw new ValidationException(
                    "Breed '" + normalizedBreed + "' is not valid for " + GetEnumCsvValue(species) + ". Valid breeds: " + string.Join(", ", validBreeds) + ".");
            }

            return canonicalBreed;
        }

        private static int GetNextId(IEnumerable<Animal> activeAnimals, IEnumerable<Animal> archivedAnimals)
        {
            // Include archived IDs so a restored or adopted animal's ID is never reused.
            return activeAnimals
                .Concat(archivedAnimals)
                .Select(a => a.Id)
                .DefaultIfEmpty(0)
                .Max() + 1;
        }

        private static string FormatCsvLine(IEnumerable<string> fields)
        {
            // Use one escape path for every saved field so commas and quotes are stored safely.
            return string.Join(",", fields.Select(EscapeCsvField).ToArray());
        }

        private static string EscapeCsvField(string value)
        {
            if (value == null) value = string.Empty;

            if (ContainsNewline(value))
            {
                throw new InvalidDataException("CSV fields cannot contain line breaks.");
            }

            bool requiresQuotes = value.IndexOfAny(new[] { ',', '"', '\r', '\n' }) >= 0;
            value = value.Replace("\"", "\"\"");

            return requiresQuotes ? "\"" + value + "\"" : value;
        }

        private static bool ContainsNewline(string value)
        {
            return value.IndexOfAny(new[] { '\r', '\n' }) >= 0;
        }

        private static List<string> ParseCsvLine(string line, string path, int lineNumber)
        {
            var fields = new List<string>();
            var field = new StringBuilder();
            bool inQuotes = false;

            // A small CSV parser is used because animal names and colours may contain commas or quotes.
            for (int i = 0; i < line.Length; i++)
            {
                char current = line[i];

                if (current == '"')
                {
                    if (inQuotes && i + 1 < line.Length && line[i + 1] == '"')
                    {
                        field.Append('"');
                        i++;
                    }
                    else
                    {
                        inQuotes = !inQuotes;
                    }
                }
                else if (current == ',' && !inQuotes)
                {
                    fields.Add(field.ToString());
                    field.Clear();
                }
                else
                {
                    field.Append(current);
                }
            }

            if (inQuotes)
            {
                throw new InvalidDataException("Invalid CSV record in '" + path + "' at line " + lineNumber + ": missing closing quote.");
            }

            fields.Add(field.ToString());
            return fields;
        }

        private static string GetEnumCsvValue<TEnum>(TEnum value) where TEnum : struct
        {
            var enumValue = (Enum)(object)value;
            MemberInfo member = enumValue.GetType().GetMember(enumValue.ToString()).FirstOrDefault();

            // DisplayAttribute names are the user-friendly values saved to CSV, such as "Small & Furry".
            if (member != null)
            {
                var display = member.GetCustomAttributes(typeof(DisplayAttribute), false)
                    .OfType<DisplayAttribute>()
                    .FirstOrDefault();

                if (display != null && !string.IsNullOrWhiteSpace(display.Name))
                {
                    return display.Name;
                }
            }

            return enumValue.ToString();
        }

        private static TEnum ParseEnumCsvValue<TEnum>(string value, string fieldName) where TEnum : struct
        {
            if (string.IsNullOrWhiteSpace(value))
            {
                throw new FormatException(fieldName + " is required.");
            }

            string normalizedValue = value.Trim();

            foreach (TEnum candidate in Enum.GetValues(typeof(TEnum)).Cast<TEnum>())
            {
                string enumName = candidate.ToString();
                string csvValue = GetEnumCsvValue(candidate);

                // Accept both internal enum names and display names so edited CSV files remain readable.
                if (string.Equals(enumName, normalizedValue, StringComparison.OrdinalIgnoreCase)
                    || string.Equals(csvValue, normalizedValue, StringComparison.OrdinalIgnoreCase))
                {
                    return candidate;
                }
            }

            throw new FormatException(fieldName + " has invalid value '" + value + "'.");
        }

        private static bool ParseYesNo(string value, string fieldName)
        {
            if (string.IsNullOrWhiteSpace(value))
            {
                throw new FormatException(fieldName + " is required.");
            }

            string normalizedValue = value.Trim();

            if (string.Equals(normalizedValue, "Yes", StringComparison.OrdinalIgnoreCase))
            {
                return true;
            }

            if (string.Equals(normalizedValue, "No", StringComparison.OrdinalIgnoreCase))
            {
                return false;
            }

            bool parsedBoolean;

            if (bool.TryParse(normalizedValue, out parsedBoolean))
            {
                return parsedBoolean;
            }

            throw new FormatException(fieldName + " must be Yes or No.");
        }

        private static bool ParseOptionalBoolean(string value, bool defaultValue, string fieldName)
        {
            if (string.IsNullOrWhiteSpace(value))
            {
                return defaultValue;
            }

            return ParseYesNo(value, fieldName);
        }

        private static DateTime? ParseOptionalDate(string value, string fieldName)
        {
            if (string.IsNullOrWhiteSpace(value))
            {
                return null;
            }

            try
            {
                return DateTime.ParseExact(value.Trim(), DateFormat, CultureInfo.InvariantCulture);
            }
            catch (FormatException ex)
            {
                throw new FormatException(fieldName + " must use " + DateFormat + " format.", ex);
            }
        }

        private static string FormatYesNo(bool value)
        {
            return value ? "Yes" : "No";
        }

        private static string FormatOptionalDate(DateTime? value)
        {
            return value.HasValue ? value.Value.ToString(DateFormat, CultureInfo.InvariantCulture) : string.Empty;
        }

        private static bool ContainsIgnoreCase(string source, string value)
        {
            if (source == null)
            {
                return false;
            }
            
            return source.IndexOf(value, StringComparison.OrdinalIgnoreCase) >= 0;
        }
    }
}

AnimalRescueManager/Views/MainPage.xaml

<Window x:Class="AnimalRescueManager.Views.MainPage"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:views="clr-namespace:AnimalRescueManager.Views"
        mc:Ignorable="d"
        Title="Animal Rescue Manager"
        Height="720"
        Width="1100"
        MinHeight="640"
        MinWidth="980">
    <Window.Resources>
        <views:EnumDisplayConverter x:Key="EnumDisplayConverter" />
        <views:BooleanYesNoConverter x:Key="BooleanYesNoConverter" />
    </Window.Resources>

    <Grid Margin="12">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <!-- Header area with the application title and manual refresh command. -->
        <DockPanel Grid.Row="0" LastChildFill="True" Margin="0,0,0,10">
            <Button DockPanel.Dock="Right" Content="Refresh" Click="RefreshButton_Click" />
            <StackPanel>
                <TextBlock Text="Animal Rescue Manager" FontSize="22" FontWeight="SemiBold" />
                <TextBlock Text="Manage active and adopted animals from one shelter file." />
            </StackPanel>
        </DockPanel>

        <TabControl x:Name="MainTabControl" Grid.Row="1">
            <!-- Active animal list and required search/sort/archive actions. -->
            <TabItem Header="Animals">
                <Grid Margin="10">
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto" />
                        <RowDefinition Height="*" />
                        <RowDefinition Height="Auto" />
                    </Grid.RowDefinitions>

                    <WrapPanel Grid.Row="0" Margin="0,0,0,10" VerticalAlignment="Center">
                        <TextBlock Text="Search" VerticalAlignment="Center" Margin="0,0,6,0" />
                        <TextBox x:Name="ActiveSearchTextBox" Width="220" Margin="0,0,8,0" KeyDown="ActiveSearchTextBox_KeyDown" />
                        <Button Content="Search" Click="SearchActiveButton_Click" />
                        <Button Content="Clear" Click="ClearActiveSearchButton_Click" />
                        <Button Content="Sort by species" MinWidth="120" Click="SortBySpeciesButton_Click" />
                        <Button Content="Three oldest" MinWidth="110" Click="ThreeOldestButton_Click" />
                    </WrapPanel>

                    <DataGrid x:Name="ActiveAnimalsGrid"
                              Grid.Row="1"
                              AutoGenerateColumns="False"
                              CanUserAddRows="False"
                              CanUserDeleteRows="False"
                              IsReadOnly="True"
                              SelectionMode="Single"
                              SelectionUnit="FullRow"
                              GridLinesVisibility="Horizontal"
                              HeadersVisibility="Column"
                              SelectionChanged="ActiveAnimalsGrid_SelectionChanged">
                        <DataGrid.Columns>
                            <DataGridTextColumn Header="ID" Binding="{Binding Id, StringFormat={}{0:D8}}" Width="84" />
                            <DataGridTextColumn Header="Species" Binding="{Binding Species, Converter={StaticResource EnumDisplayConverter}}" Width="110" />
                            <DataGridTextColumn Header="Name" Binding="{Binding Name}" Width="130" />
                            <DataGridTextColumn Header="Gender" Binding="{Binding Gender}" Width="70" />
                            <DataGridTextColumn Header="Spayed" Binding="{Binding Spayed, Converter={StaticResource BooleanYesNoConverter}}" Width="70" />
                            <DataGridTextColumn Header="Breed" Binding="{Binding Breed}" Width="120" />
                            <DataGridTextColumn Header="Colour" Binding="{Binding Colour}" Width="100" />
                            <DataGridTextColumn Header="Birthday" Binding="{Binding Birthday, StringFormat={}{0:dd/MM/yyyy}}" Width="100" />
                            <DataGridTextColumn Header="Vaccines" Binding="{Binding VaccineStatus, Converter={StaticResource EnumDisplayConverter}}" Width="100" />
                            <DataGridTextColumn Header="ID Tag" Binding="{Binding Identification, Converter={StaticResource BooleanYesNoConverter}}" Width="70" />
                            <DataGridTextColumn Header="ID Type" Binding="{Binding IdType, Converter={StaticResource EnumDisplayConverter}}" Width="110" />
                            <DataGridTextColumn Header="ID Number" Binding="{Binding IdNumber}" Width="120" />
                            <DataGridTextColumn Header="Fee" Binding="{Binding AdoptionFee, StringFormat={}{0:C0}}" Width="70" />
                            <DataGridTextColumn Header="Adopted" Binding="{Binding AdoptedDate, StringFormat={}{0:dd/MM/yyyy}}" Width="100" />
                        </DataGrid.Columns>
                    </DataGrid>

                    <Grid Grid.Row="2" Margin="0,10,0,0">
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                        </Grid.RowDefinitions>

                        <DockPanel Grid.Row="0" LastChildFill="False">
                            <StackPanel DockPanel.Dock="Left" Orientation="Horizontal">
                                <Button Content="New" Click="NewAnimalButton_Click" />
                                <Button Content="Edit selected" MinWidth="110" Click="EditSelectedButton_Click" />
                                <Button Content="Remove selected" MinWidth="130" Click="RemoveSelectedButton_Click" />
                            </StackPanel>
                            <StackPanel DockPanel.Dock="Right" Orientation="Horizontal" VerticalAlignment="Center">
                                <TextBlock Text="Adopted date" VerticalAlignment="Center" Margin="0,0,6,0" />
                                <DatePicker x:Name="ArchiveAdoptedDatePicker" Width="130" Margin="0,0,8,0" />
                                <Button Content="Archive selected" MinWidth="130" Click="ArchiveSelectedButton_Click" />
                            </StackPanel>
                        </DockPanel>

                        <StackPanel Grid.Row="1" Orientation="Horizontal" Margin="0,8,0,0">
                            <TextBlock Text="Remove by ID" VerticalAlignment="Center" Margin="0,0,6,0" />
                            <TextBox x:Name="RemoveByIdTextBox" Width="95" Margin="0,0,6,0" KeyDown="RemoveByIdTextBox_KeyDown" />
                            <Button Content="Remove by ID" MinWidth="120" Click="RemoveByIdButton_Click" />
                        </StackPanel>
                    </Grid>
                </Grid>
            </TabItem>

            <!-- Add and edit form for the full animal record. -->
            <TabItem Header="Add / Edit">
                <ScrollViewer VerticalScrollBarVisibility="Auto">
                    <Grid Margin="10" MaxWidth="900" HorizontalAlignment="Left">
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="170" />
                            <ColumnDefinition Width="300" />
                        </Grid.ColumnDefinitions>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="Auto" />
                        </Grid.RowDefinitions>

                        <TextBlock x:Name="FormModeTextBlock"
                                   Grid.Row="0"
                                   Grid.ColumnSpan="2"
                                   Text="Adding new animal"
                                   FontSize="18"
                                   FontWeight="SemiBold"
                                   Margin="0,0,0,12" />

                        <TextBlock Grid.Row="1" Grid.Column="0" Text="ID" VerticalAlignment="Center" />
                        <TextBox x:Name="IdTextBox" Grid.Row="1" Grid.Column="1" IsReadOnly="True" />

                        <TextBlock Grid.Row="2" Grid.Column="0" Text="Name" VerticalAlignment="Center" />
                        <TextBox x:Name="NameTextBox" Grid.Row="2" Grid.Column="1" />

                        <TextBlock Grid.Row="3" Grid.Column="0" Text="Species" VerticalAlignment="Center" />
                        <ComboBox x:Name="SpeciesComboBox" Grid.Row="3" Grid.Column="1" DisplayMemberPath="Text" SelectedValuePath="Value" SelectionChanged="SpeciesComboBox_SelectionChanged" />

                        <TextBlock Grid.Row="4" Grid.Column="0" Text="Gender" VerticalAlignment="Center" />
                        <ComboBox x:Name="GenderComboBox" Grid.Row="4" Grid.Column="1" DisplayMemberPath="Text" SelectedValuePath="Value" />

                        <TextBlock Grid.Row="5" Grid.Column="0" Text="Spayed" VerticalAlignment="Center" />
                        <CheckBox x:Name="SpayedCheckBox" Grid.Row="5" Grid.Column="1" VerticalAlignment="Center" Margin="0,2,0,8" />

                        <TextBlock Grid.Row="6" Grid.Column="0" Text="Breed" VerticalAlignment="Center" />
                        <ComboBox x:Name="BreedComboBox" Grid.Row="6" Grid.Column="1" />

                        <TextBlock Grid.Row="7" Grid.Column="0" Text="Colour" VerticalAlignment="Center" />
                        <TextBox x:Name="ColourTextBox" Grid.Row="7" Grid.Column="1" />

                        <TextBlock Grid.Row="8" Grid.Column="0" Text="Birthday" VerticalAlignment="Center" />
                        <DatePicker x:Name="BirthdayDatePicker" Grid.Row="8" Grid.Column="1" SelectedDateChanged="BirthdayDatePicker_SelectedDateChanged" />

                        <TextBlock Grid.Row="9" Grid.Column="0" Text="Vaccine status" VerticalAlignment="Center" />
                        <ComboBox x:Name="VaccineStatusComboBox" Grid.Row="9" Grid.Column="1" DisplayMemberPath="Text" SelectedValuePath="Value" />

                        <TextBlock Grid.Row="10" Grid.Column="0" Text="Adoption fee" VerticalAlignment="Center" />
                        <TextBlock x:Name="AdoptionFeeTextBlock" Grid.Row="10" Grid.Column="1" VerticalAlignment="Center" FontWeight="SemiBold" />

                        <TextBlock Grid.Row="11" Grid.Column="0" Text="Identification" VerticalAlignment="Center" />
                        <CheckBox x:Name="IdentificationCheckBox"
                                  Grid.Row="11"
                                  Grid.Column="1"
                                  VerticalAlignment="Center"
                                  Margin="0,2,0,8"
                                  Checked="IdentificationCheckBox_Changed"
                                  Unchecked="IdentificationCheckBox_Changed" />

                        <TextBlock Grid.Row="12" Grid.Column="0" Text="ID type" VerticalAlignment="Center" />
                        <ComboBox x:Name="IdTypeComboBox" Grid.Row="12" Grid.Column="1" DisplayMemberPath="Text" SelectedValuePath="Value" />

                        <TextBlock Grid.Row="13" Grid.Column="0" Text="ID number" VerticalAlignment="Center" />
                        <TextBox x:Name="IdNumberTextBox" Grid.Row="13" Grid.Column="1" />

                        <TextBlock Grid.Row="14" Grid.Column="0" Text="Adopted date" VerticalAlignment="Center" />
                        <DockPanel Grid.Row="14" Grid.Column="1" LastChildFill="True">
                            <Button DockPanel.Dock="Right" Content="Clear" MinWidth="60" Click="ClearAdoptedDateButton_Click" />
                            <DatePicker x:Name="AdoptedDatePicker" Margin="0,2,8,8" />
                        </DockPanel>

                        <StackPanel Grid.Row="15" Grid.Column="0" Grid.ColumnSpan="2" Orientation="Horizontal" Margin="0,14,0,0">
                            <Button Content="Save animal" MinWidth="110" Click="SaveAnimalButton_Click" />
                            <Button Content="Clear form" MinWidth="110" Click="ClearFormButton_Click" />
                        </StackPanel>
                    </Grid>
                </ScrollViewer>
            </TabItem>

            <!-- Archived animal search, restore, and bulk archive actions. -->
            <TabItem Header="Archives">
                <Grid Margin="10">
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto" />
                        <RowDefinition Height="*" />
                        <RowDefinition Height="Auto" />
                    </Grid.RowDefinitions>

                    <WrapPanel Grid.Row="0" Margin="0,0,0,10" VerticalAlignment="Center">
                        <TextBlock Text="Archive date from" VerticalAlignment="Center" Margin="0,0,6,0" />
                        <DatePicker x:Name="ArchiveStartDatePicker" Width="130" Margin="0,0,8,0" />
                        <TextBlock Text="to" VerticalAlignment="Center" Margin="0,0,6,0" />
                        <DatePicker x:Name="ArchiveEndDatePicker" Width="130" Margin="0,0,8,0" />
                        <Button Content="Search archives" MinWidth="130" Click="SearchArchivesButton_Click" />
                        <Button Content="Show all" Click="ShowAllArchivesButton_Click" />
                    </WrapPanel>

                    <DataGrid x:Name="ArchivedAnimalsGrid"
                              Grid.Row="1"
                              AutoGenerateColumns="False"
                              CanUserAddRows="False"
                              CanUserDeleteRows="False"
                              IsReadOnly="True"
                              SelectionMode="Single"
                              SelectionUnit="FullRow"
                              GridLinesVisibility="Horizontal"
                              HeadersVisibility="Column">
                        <DataGrid.Columns>
                            <DataGridTextColumn Header="ID" Binding="{Binding Id, StringFormat={}{0:D8}}" Width="84" />
                            <DataGridTextColumn Header="Species" Binding="{Binding Species, Converter={StaticResource EnumDisplayConverter}}" Width="110" />
                            <DataGridTextColumn Header="Name" Binding="{Binding Name}" Width="130" />
                            <DataGridTextColumn Header="Gender" Binding="{Binding Gender}" Width="70" />
                            <DataGridTextColumn Header="Spayed" Binding="{Binding Spayed, Converter={StaticResource BooleanYesNoConverter}}" Width="70" />
                            <DataGridTextColumn Header="Breed" Binding="{Binding Breed}" Width="120" />
                            <DataGridTextColumn Header="Colour" Binding="{Binding Colour}" Width="100" />
                            <DataGridTextColumn Header="Birthday" Binding="{Binding Birthday, StringFormat={}{0:dd/MM/yyyy}}" Width="100" />
                            <DataGridTextColumn Header="Vaccines" Binding="{Binding VaccineStatus, Converter={StaticResource EnumDisplayConverter}}" Width="100" />
                            <DataGridTextColumn Header="Fee" Binding="{Binding AdoptionFee, StringFormat={}{0:C0}}" Width="70" />
                            <DataGridTextColumn Header="Adopted" Binding="{Binding AdoptedDate, StringFormat={}{0:dd/MM/yyyy}}" Width="100" />
                            <DataGridTextColumn Header="Archived" Binding="{Binding ArchiveDate, StringFormat={}{0:dd/MM/yyyy}}" Width="100" />
                        </DataGrid.Columns>
                    </DataGrid>

                    <DockPanel Grid.Row="2" Margin="0,10,0,0" LastChildFill="False">
                        <StackPanel DockPanel.Dock="Left" Orientation="Horizontal">
                            <Button Content="Restore selected" MinWidth="130" Click="RestoreSelectedButton_Click" />
                        </StackPanel>
                        <StackPanel DockPanel.Dock="Right" Orientation="Horizontal">
                            <Button Content="Archive adopted 3+ months" MinWidth="190" Click="ArchiveOldAdoptedButton_Click" />
                        </StackPanel>
                    </DockPanel>
                </Grid>
            </TabItem>

            <!-- Built-in usage notes required for the submitted application. -->
            <TabItem Header="Help">
                <ScrollViewer VerticalScrollBarVisibility="Auto">
                    <StackPanel Margin="16" MaxWidth="820" HorizontalAlignment="Left">
                        <TextBlock Text="Usage Instructions" FontSize="18" FontWeight="SemiBold" Margin="0,0,0,10" />
                        <TextBlock TextWrapping="Wrap" Margin="0,0,0,8"
                                   Text="Animals tab: view active animals, search by name or species, sort by species, show the three oldest animals in each species, remove selected animals, or archive an adopted animal." />
                        <TextBlock TextWrapping="Wrap" Margin="0,0,0,8"
                                   Text="Add / Edit tab: enter all required animal details and save. Select an active animal and choose Edit selected to update it. Adoption fee is calculated from the birthday." />
                        <TextBlock TextWrapping="Wrap" Margin="0,0,0,8"
                                   Text="Archives tab: search adopted animals by archive date, restore selected archived animals, or archive all active animals whose adopted date is at least three months old." />
                        <TextBlock TextWrapping="Wrap" Margin="0,0,0,8"
                                   Text="Files: active animals are stored in animals.csv and archived animals are stored in archives.csv beside the running application." />
                    </StackPanel>
                </ScrollViewer>
            </TabItem>
        </TabControl>

        <!-- Status messages give immediate feedback after validation, saves, and searches. -->
        <Grid Grid.Row="2" Margin="0,10,0,0">
            <TextBlock x:Name="StatusTextBlock" Text="Ready" />
        </Grid>
    </Grid>
</Window>

AnimalRescueManager/Views/MainPage.xaml.cs

using AnimalRescueManager.Models;
using AnimalRescueManager.Services;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;

namespace AnimalRescueManager.Views
{
    /// <summary>
    /// Main WPF window for viewing, editing, archiving, and restoring animal records.
    /// </summary>
    public partial class MainPage : Window
    {
        private readonly DataService _dataService;

        // Null means the form is adding a new animal; a value means the form is editing that ID.
        private int? _editingAnimalId;

        /// <summary>
        /// Initializes the window controls and loads the current CSV data.
        /// </summary>
        public MainPage()
        {
            InitializeComponent();

            _dataService = new DataService();
            InitializeComboBoxes();
            InitializeDateFields();
            ClearForm();
            RefreshAll();
        }

        private void InitializeComboBoxes()
        {
            SpeciesComboBox.ItemsSource = CreateEnumOptions<Species>();
            GenderComboBox.ItemsSource = CreateEnumOptions<Gender>();
            VaccineStatusComboBox.ItemsSource = CreateEnumOptions<VaccineStatus>();
            IdTypeComboBox.ItemsSource = CreateEnumOptions<IdentificationType>();
        }

        private void InitializeDateFields()
        {
            ArchiveAdoptedDatePicker.SelectedDate = DateTime.Today;
            ArchiveStartDatePicker.SelectedDate = DateTime.Today.AddMonths(-1);
            ArchiveEndDatePicker.SelectedDate = DateTime.Today;
        }

        private void RefreshAll()
        {
            try
            {
                // Read both files together so the counts and grids stay in sync after each action.
                var activeAnimals = _dataService.GetAllAnimals();
                var archivedAnimals = _dataService.GetArchivedAnimals();

                ActiveAnimalsGrid.ItemsSource = activeAnimals;
                ArchivedAnimalsGrid.ItemsSource = archivedAnimals;

                SetStatus(activeAnimals.Count + " active animals, " + archivedAnimals.Count + " archived animals.");
            }
            catch (Exception ex)
            {
                ShowError(ex);
            }
        }

        private void BindActiveAnimals(List<Animal> animals, string message)
        {
            ActiveAnimalsGrid.ItemsSource = animals;
            SetStatus(message + " " + animals.Count + " active animals displayed.");
        }

        private void BindArchivedAnimals(List<Animal> animals, string message)
        {
            ArchivedAnimalsGrid.ItemsSource = animals;
            SetStatus(message + " " + animals.Count + " archived animals displayed.");
        }

        private void SearchActiveButton_Click(object sender, RoutedEventArgs e)
        {
            try
            {
                BindActiveAnimals(_dataService.SearchAnimals(ActiveSearchTextBox.Text), "Search complete.");
            }
            catch (Exception ex)
            {
                ShowError(ex);
            }
        }

        private void ActiveSearchTextBox_KeyDown(object sender, KeyEventArgs e)
        {
            if (e.Key == Key.Enter)
            {
                SearchActiveButton_Click(sender, e);
            }
        }

        private void RemoveByIdTextBox_KeyDown(object sender, KeyEventArgs e)
        {
            if (e.Key == Key.Enter)
            {
                RemoveByIdButton_Click(sender, e);
            }
        }

        private void ClearActiveSearchButton_Click(object sender, RoutedEventArgs e)
        {
            ActiveSearchTextBox.Clear();
            RefreshAll();
        }

        private void SortBySpeciesButton_Click(object sender, RoutedEventArgs e)
        {
            try
            {
                BindActiveAnimals(_dataService.GetAnimalsSortedBySpecies(), "Sorted by species.");
            }
            catch (Exception ex)
            {
                ShowError(ex);
            }
        }

        private void ThreeOldestButton_Click(object sender, RoutedEventArgs e)
        {
            try
            {
                BindActiveAnimals(_dataService.GetThreeOldestBySpecies(), "Three oldest per species.");
            }
            catch (Exception ex)
            {
                ShowError(ex);
            }
        }

        private void NewAnimalButton_Click(object sender, RoutedEventArgs e)
        {
            ClearForm();
            MainTabControl.SelectedIndex = 1;
        }

        private void EditSelectedButton_Click(object sender, RoutedEventArgs e)
        {
            var animal = GetSelectedActiveAnimal();

            if (animal == null)
            {
                ShowMessage("Select an active animal to edit.");
                return;
            }

            try
            {
                LoadAnimalIntoForm(animal);
                MainTabControl.SelectedIndex = 1;
            }
            catch (Exception ex)
            {
                ShowError(ex);
            }
        }

        private void RemoveSelectedButton_Click(object sender, RoutedEventArgs e)
        {
            var animal = GetSelectedActiveAnimal();

            if (animal == null)
            {
                ShowMessage("Select an active animal to remove.");
                return;
            }

            var result = MessageBox.Show(
                "Remove " + animal.Name + " from the active animal file?",
                "Remove Animal",
                MessageBoxButton.YesNo,
                MessageBoxImage.Warning);

            if (result != MessageBoxResult.Yes)
            {
                return;
            }

            try
            {
                _dataService.RemoveAnimal(animal.Id);

                if (_editingAnimalId == animal.Id)
                {
                    ClearForm();
                }

                RefreshAll();
                SetStatus("Removed " + animal.Name + ".");
            }
            catch (Exception ex)
            {
                ShowError(ex);
            }
        }

        private void RemoveByIdButton_Click(object sender, RoutedEventArgs e)
        {
            int id;

            if (!TryGetRemoveAnimalId(out id))
            {
                return;
            }

            var result = MessageBox.Show(
                "Remove animal " + id.ToString("D8", CultureInfo.InvariantCulture) + " from the active animal file?",
                "Remove Animal",
                MessageBoxButton.YesNo,
                MessageBoxImage.Warning);

            if (result != MessageBoxResult.Yes)
            {
                return;
            }

            try
            {
                _dataService.RemoveAnimal(id);

                if (_editingAnimalId == id)
                {
                    ClearForm();
                }

                RemoveByIdTextBox.Clear();
                RefreshAll();
                SetStatus("Removed animal " + id.ToString("D8", CultureInfo.InvariantCulture) + ".");
            }
            catch (Exception ex)
            {
                ShowError(ex);
            }
        }

        private void ArchiveSelectedButton_Click(object sender, RoutedEventArgs e)
        {
            var animal = GetSelectedActiveAnimal();

            if (animal == null)
            {
                ShowMessage("Select an active animal to archive.");
                return;
            }

            if (!ArchiveAdoptedDatePicker.SelectedDate.HasValue)
            {
                ShowMessage("Select the adopted date before archiving.");
                return;
            }

            try
            {
                _dataService.ArchiveAnimal(animal.Id, ArchiveAdoptedDatePicker.SelectedDate.Value);

                if (_editingAnimalId == animal.Id)
                {
                    ClearForm();
                }

                RefreshAll();
                SetStatus("Archived " + animal.Name + ".");
            }
            catch (Exception ex)
            {
                ShowError(ex);
            }
        }

        private void SaveAnimalButton_Click(object sender, RoutedEventArgs e)
        {
            try
            {
                var animal = BuildAnimalFromForm();
                bool editing = _editingAnimalId.HasValue;

                if (editing)
                {
                    _dataService.UpdateAnimal(animal);
                }
                else
                {
                    _dataService.AddAnimal(animal);
                }

                RefreshAll();
                ClearForm();
                MainTabControl.SelectedIndex = 0;
                SetStatus(editing ? "Animal updated." : "Animal added.");
            }
            catch (Exception ex)
            {
                ShowError(ex);
            }
        }

        private void ClearFormButton_Click(object sender, RoutedEventArgs e)
        {
            ClearForm();
        }

        private void ClearAdoptedDateButton_Click(object sender, RoutedEventArgs e)
        {
            AdoptedDatePicker.SelectedDate = null;
        }

        private void SearchArchivesButton_Click(object sender, RoutedEventArgs e)
        {
            if (!ArchiveStartDatePicker.SelectedDate.HasValue || !ArchiveEndDatePicker.SelectedDate.HasValue)
            {
                ShowMessage("Select both archive search dates.");
                return;
            }

            try
            {
                var results = _dataService.SearchArchivedAnimals(
                    ArchiveStartDatePicker.SelectedDate.Value,
                    ArchiveEndDatePicker.SelectedDate.Value);

                BindArchivedAnimals(results, "Archive search complete.");
            }
            catch (Exception ex)
            {
                ShowError(ex);
            }
        }

        private void ShowAllArchivesButton_Click(object sender, RoutedEventArgs e)
        {
            try
            {
                BindArchivedAnimals(_dataService.GetArchivedAnimals(), "Showing all archives.");
            }
            catch (Exception ex)
            {
                ShowError(ex);
            }
        }

        private void RestoreSelectedButton_Click(object sender, RoutedEventArgs e)
        {
            var animal = ArchivedAnimalsGrid.SelectedItem as Animal;

            if (animal == null)
            {
                ShowMessage("Select an archived animal to restore.");
                return;
            }

            try
            {
                _dataService.RestoreAnimal(animal.Id);
                RefreshAll();
                SetStatus("Restored " + animal.Name + ".");
            }
            catch (Exception ex)
            {
                ShowError(ex);
            }
        }

        private void ArchiveOldAdoptedButton_Click(object sender, RoutedEventArgs e)
        {
            try
            {
                int archivedCount = _dataService.ArchiveAnimalsAdoptedAtLeastThreeMonthsAgo();
                RefreshAll();
                SetStatus("Archived " + archivedCount + " animals adopted at least three months ago.");
            }
            catch (Exception ex)
            {
                ShowError(ex);
            }
        }

        private void RefreshButton_Click(object sender, RoutedEventArgs e)
        {
            RefreshAll();
        }

        private void ActiveAnimalsGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            var animal = GetSelectedActiveAnimal();

            if (animal != null)
            {
                SetStatus("Selected " + animal.Name + " (" + animal.Id.ToString("D8", CultureInfo.InvariantCulture) + ").");
            }
        }

        private void IdentificationCheckBox_Changed(object sender, RoutedEventArgs e)
        {
            UpdateIdentificationControls();
        }

        private void BirthdayDatePicker_SelectedDateChanged(object sender, SelectionChangedEventArgs e)
        {
            UpdateFeePreview();
        }

        private void SpeciesComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            UpdateBreedOptions(null);
        }

        private void ClearForm()
        {
            // Reset the form to a valid add-new state so the next save creates a new ID.
            _editingAnimalId = null;
            FormModeTextBlock.Text = "Adding new animal";
            IdTextBox.Text = "New";

            NameTextBox.Clear();
            ColourTextBox.Clear();
            IdNumberTextBox.Clear();

            SpeciesComboBox.SelectedIndex = 0;
            UpdateBreedOptions(null);
            GenderComboBox.SelectedIndex = 0;
            VaccineStatusComboBox.SelectedIndex = 0;
            IdTypeComboBox.SelectedIndex = 0;

            SpayedCheckBox.IsChecked = false;
            IdentificationCheckBox.IsChecked = false;
            BirthdayDatePicker.SelectedDate = DateTime.Today;
            AdoptedDatePicker.SelectedDate = null;

            UpdateIdentificationControls();
            UpdateFeePreview();
        }

        private void LoadAnimalIntoForm(Animal animal)
        {
            // Normalize first in case the CSV contains a different casing for the selected breed.
            string normalizedBreed = _dataService.NormalizeBreed(animal.Species, animal.Breed);

            _editingAnimalId = animal.Id;
            FormModeTextBlock.Text = "Editing " + animal.Name;
            IdTextBox.Text = animal.Id.ToString("D8", CultureInfo.InvariantCulture);

            NameTextBox.Text = animal.Name;
            SpeciesComboBox.SelectedValue = animal.Species;
            GenderComboBox.SelectedValue = animal.Gender;
            SpayedCheckBox.IsChecked = animal.Spayed;
            UpdateBreedOptions(normalizedBreed);
            ColourTextBox.Text = animal.Colour;
            BirthdayDatePicker.SelectedDate = animal.Birthday;
            VaccineStatusComboBox.SelectedValue = animal.VaccineStatus;
            IdentificationCheckBox.IsChecked = animal.Identification;
            IdTypeComboBox.SelectedValue = animal.IdType.HasValue ? (object)animal.IdType.Value : null;
            IdNumberTextBox.Text = animal.IdNumber;
            AdoptedDatePicker.SelectedDate = animal.AdoptedDate;

            UpdateIdentificationControls();
            UpdateFeePreview();
        }

        private Animal BuildAnimalFromForm()
        {
            if (!BirthdayDatePicker.SelectedDate.HasValue)
            {
                throw new ValidationException("Birthday is required.");
            }

            bool hasIdentification = IdentificationCheckBox.IsChecked == true;

            if (hasIdentification && IdTypeComboBox.SelectedValue == null)
            {
                throw new ValidationException("Identification type is required when identification is marked Yes.");
            }

            Species species = GetSelectedEnum<Species>(SpeciesComboBox, "Species");

            // New animals use ID 0 here; DataService assigns the final generated ID when saving.
            return new Animal
            {
                Id = _editingAnimalId.GetValueOrDefault(),
                Species = species,
                Name = NameTextBox.Text,
                Gender = GetSelectedEnum<Gender>(GenderComboBox, "Gender"),
                Spayed = SpayedCheckBox.IsChecked == true,
                Breed = GetSelectedBreed(species),
                Colour = ColourTextBox.Text,
                Birthday = BirthdayDatePicker.SelectedDate.Value.Date,
                VaccineStatus = GetSelectedEnum<VaccineStatus>(VaccineStatusComboBox, "Vaccine status"),
                Identification = hasIdentification,
                IdType = hasIdentification ? (IdentificationType?)GetSelectedEnum<IdentificationType>(IdTypeComboBox, "Identification type") : null,
                IdNumber = hasIdentification ? IdNumberTextBox.Text : string.Empty,
                AdoptionFee = _dataService.CalculateAdoptionFee(BirthdayDatePicker.SelectedDate.Value),
                IsArchived = false,
                AdoptedDate = AdoptedDatePicker.SelectedDate.HasValue ? (DateTime?)AdoptedDatePicker.SelectedDate.Value.Date : null,
                ArchiveDate = null
            };
        }

        private TEnum GetSelectedEnum<TEnum>(ComboBox comboBox, string fieldName) where TEnum : struct
        {
            if (comboBox.SelectedValue == null)
            {
                throw new ValidationException(fieldName + " is required.");
            }

            return (TEnum)comboBox.SelectedValue;
        }

        private string GetSelectedBreed(Species species)
        {
            var selectedBreed = BreedComboBox.SelectedItem as string;

            if (string.IsNullOrWhiteSpace(selectedBreed))
            {
                throw new ValidationException("Breed is required.");
            }

            return _dataService.NormalizeBreed(species, selectedBreed);
        }

        private bool TryGetRemoveAnimalId(out int id)
        {
            id = 0;

            if (string.IsNullOrWhiteSpace(RemoveByIdTextBox.Text))
            {
                ShowMessage("Enter an animal ID to remove.");
                return false;
            }

            if (!int.TryParse(RemoveByIdTextBox.Text.Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out id) || id <= 0)
            {
                ShowMessage("Enter a valid positive animal ID.");
                return false;
            }

            return true;
        }

        private Animal GetSelectedActiveAnimal()
        {
            return ActiveAnimalsGrid.SelectedItem as Animal;
        }

        private void UpdateBreedOptions(string selectedBreed)
        {
            if (_dataService == null || BreedComboBox == null || SpeciesComboBox.SelectedValue == null)
            {
                return;
            }

            Species species = GetSelectedEnum<Species>(SpeciesComboBox, "Species");
            var breeds = _dataService.GetValidBreeds(species);

            // Breed choices are species-specific, so changing species rebuilds the dropdown.
            BreedComboBox.ItemsSource = breeds;

            if (!string.IsNullOrWhiteSpace(selectedBreed))
            {
                BreedComboBox.SelectedItem = _dataService.NormalizeBreed(species, selectedBreed);
            }
            else if (breeds.Count > 0)
            {
                BreedComboBox.SelectedIndex = 0;
            }
        }

        private void UpdateIdentificationControls()
        {
            bool hasIdentification = IdentificationCheckBox.IsChecked == true;

            // Identification details are required only when the animal has an ID tag or chip.
            IdTypeComboBox.IsEnabled = hasIdentification;
            IdNumberTextBox.IsEnabled = hasIdentification;

            if (hasIdentification)
            {
                if (IdTypeComboBox.SelectedIndex < 0)
                {
                    IdTypeComboBox.SelectedIndex = 0;
                }
            }
            else
            {
                IdTypeComboBox.SelectedValue = null;
                IdNumberTextBox.Clear();
            }
        }

        private void UpdateFeePreview()
        {
            if (BirthdayDatePicker.SelectedDate.HasValue)
            {
                // Show the same calculated fee that will be saved by DataService.
                decimal fee = _dataService.CalculateAdoptionFee(BirthdayDatePicker.SelectedDate.Value);
                AdoptionFeeTextBlock.Text = fee.ToString("C0", CultureInfo.CurrentCulture);
            }
            else
            {
                AdoptionFeeTextBlock.Text = string.Empty;
            }
        }

        private void SetStatus(string message)
        {
            StatusTextBlock.Text = message;
        }

        private void ShowMessage(string message)
        {
            MessageBox.Show(message, "Animal Rescue Manager", MessageBoxButton.OK, MessageBoxImage.Information);
            SetStatus(message);
        }

        private void ShowError(Exception ex)
        {
            MessageBox.Show(ex.Message, "Animal Rescue Manager", MessageBoxButton.OK, MessageBoxImage.Error);
            SetStatus("Error: " + ex.Message);
        }

        private static List<EnumOption> CreateEnumOptions<TEnum>() where TEnum : struct
        {
            // WPF displays Text to the user while SelectedValue stores the enum value.
            return Enum.GetValues(typeof(TEnum))
                .Cast<TEnum>()
                .Select(value => new EnumOption
                {
                    Text = EnumDisplayConverter.GetDisplayText((Enum)(object)value),
                    Value = value
                })
                .ToList();
        }
    }

    /// <summary>
    /// Simple display/value pair used by ComboBox controls for enum selections.
    /// </summary>
    public class EnumOption
    {
        /// <summary>
        /// User-facing text shown in the dropdown.
        /// </summary>
        public string Text { get; set; }

        /// <summary>
        /// Backing enum value saved by the application.
        /// </summary>
        public object Value { get; set; }
    }

    /// <summary>
    /// Converts enum values to their DisplayAttribute names for WPF bindings.
    /// </summary>
    public class EnumDisplayConverter : IValueConverter
    {
        /// <summary>
        /// Converts an enum value into display text for a grid or combo box.
        /// </summary>
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value == null)
            {
                return string.Empty;
            }

            var enumValue = value as Enum;

            if (enumValue == null)
            {
                return value.ToString();
            }

            return GetDisplayText(enumValue);
        }

        /// <summary>
        /// One-way converter only; the application reads selected enum values directly.
        /// </summary>
        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return Binding.DoNothing;
        }

        /// <summary>
        /// Reads a DisplayAttribute name when present, otherwise falls back to the enum name.
        /// </summary>
        public static string GetDisplayText(Enum value)
        {
            MemberInfo member = value.GetType().GetMember(value.ToString()).FirstOrDefault();

            if (member != null)
            {
                var display = member.GetCustomAttributes(typeof(DisplayAttribute), false)
                    .OfType<DisplayAttribute>()
                    .FirstOrDefault();

                if (display != null && !string.IsNullOrWhiteSpace(display.Name))
                {
                    return display.Name;
                }
            }

            return value.ToString();
        }
    }

    /// <summary>
    /// Displays boolean values as Yes or No in read-only grids.
    /// </summary>
    public class BooleanYesNoConverter : IValueConverter
    {
        /// <summary>
        /// Converts a boolean value into Yes or No text.
        /// </summary>
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value is bool)
            {
                return (bool)value ? "Yes" : "No";
            }

            return string.Empty;
        }

        /// <summary>
        /// One-way converter only because grid values are not edited in place.
        /// </summary>
        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return Binding.DoNothing;
        }
    }
}