/* 
 * Copyright (c)2009-2025 DemiVision, LLC. All Rights Reserved. The information 
 * herein is the CONFIDENTIAL and PROPRIETARY information of DemiVision, LLC. 
 */

using System.Text.Json.Serialization;
using Plugin.Firebase.Firestore;

using DXLib.Data;
using DXLib.Data.Model;

using DXLib.Utils;

namespace iStatVball3;

/*
 * Represents one season (e.g., Fall 2024) for a parent team. Season is the parent for most core objects (players,
 * matches, etc).
 */
public class Season : DXImageModel
{
	/* Constants */
	public const string CollectionKey = "Seasons";

    /* Properties */

    // Required
    [FirestoreProperty("StartDate")] public DateTimeOffset StartDate { get; set; }
    [FirestoreProperty("EndDate")] public DateTimeOffset EndDate { get; set; }

    [FirestoreProperty("Name")] public string Name { get; set; }

    // Optional
    [FirestoreProperty("Description")] public string Description { get; set; }
    [FirestoreProperty("Result")] public string Result { get; set; }
    [FirestoreProperty("Notes")] public string Notes { get; set; }

    // Record
    [FirestoreProperty("Won")] public int Won { get; set; }
    [FirestoreProperty("Lost")] public int Lost { get; set; }
    [FirestoreProperty("Tied")] public int Tied { get; set; }

    // Recorded at least one match?
    [FirestoreProperty("Consumed")] public bool? Consumed { get; set; }

    // Purchase (FK)
    [FirestoreProperty("PurchaseId")] public string PurchaseId { get; set; }

    // Parent (FK)
    [FirestoreProperty("TeamId")] public string TeamId { get; set; }

	/* Ignored */
	[JsonIgnore] public Color Color => Team.Color;
	[JsonIgnore] public int MatchCount => (Won + Lost + Tied);

	[JsonIgnore] public Organization Organization => Team.Organization;

	[JsonIgnore] public bool IsSample => Team.IsSample;
	[JsonIgnore] public bool IsHighSchool => Organization.IsHighSchool;

	[JsonIgnore] public bool IsPopulated { get; private set; }
	[JsonIgnore] public bool IsConsumed => Consumed is true;

	// Children

	// Roster
	[JsonIgnore] public List<Player> Players { get; private set; }
	[JsonIgnore] public List<Lineup> Lineups { get; private set; }

	// Matches
	[JsonIgnore] public List<Match> Matches { get; private set; }
	[JsonIgnore] public List<Set> Sets { get; private set; }
	[JsonIgnore] public List<Tournament> Tournaments { get; private set; }

	// Tags
	[JsonIgnore] public List<Coach> Coaches { get; private set; }
	[JsonIgnore] public List<Statistician> Statisticians { get; private set; }
	[JsonIgnore] public List<Opponent> Opponents { get; private set; }
	[JsonIgnore] public List<Venue> Venues { get; private set; }

	// Tags as object list
	[JsonIgnore] public List<DXModel> PlayerList => Players.Cast<DXModel>().ToList();
	[JsonIgnore] public List<DXModel> LineupList => Lineups.Cast<DXModel>().ToList();

	[JsonIgnore] public List<DXModel> TournamentList => Tournaments.Cast<DXModel>().ToList();
	[JsonIgnore] public List<DXModel> CoachList => Coaches.Cast<DXModel>().ToList();
	[JsonIgnore] public List<DXModel> StatisticianList => Statisticians.Cast<DXModel>().ToList();
	[JsonIgnore] public List<DXModel> OpponentList => Opponents.Cast<DXModel>().ToList();
	[JsonIgnore] public List<DXModel> VenueList => Venues.Cast<DXModel>().ToList();

	// Parent
	public Team Team { get; set; }

	/* Methods */
	public Season()
	{
		BaseCollectionKey = CollectionKey;

		// Allocate containers
		Players = [];
		Lineups = [];

		Matches = [];
		Sets = [];
		Tournaments = [];

		Coaches = [];
		Statisticians = [];
		Opponents = [];
		Venues = [];
	}

	// Tests equality based on unique identifier
	public override bool Equals( object obj )
	{
		return (obj is Season season) && season.UniqueId.Equals( UniqueId );
	}

	// Generates unique hash code, required for list de-duping
	public override int GetHashCode()
	{
		return UniqueId.GetHashCode();
	}

	// Builds season name based on start/end dates ('Fall 2024', '2024-2025')
	public static string BuildName( DateTimeOffset start, DateTimeOffset end, bool showSeason )
	{
		string year1 = start.Year.ToString();
		string year2 = end.Year.ToString();

		// Include 'Winter/Spring/Etc'
		if ( showSeason )
		{
			string season = start.Month switch
			{
				// Winter (Nov-Jan)
				11 or 12 or 1 => DXString.Get( "season.winter" ),
				
				// Spring (Feb-Apr)
				2 or 3 or 4 => DXString.Get( "season.spring" ),
				
				// Summer (May-Jul)
				5 or 6 or 7 => DXString.Get( "season.summer" ),
				
				// Fall (Aug-Oct)
				8 or 9 or 10 => DXString.Get( "season.fall" ),
				
				_ => null
			};

			// 'Fall 2024'
			return $"{season} {year1}";
		}

		// '2024-2025'
		return (year1 == year2) ? year1 : $"{year1}-{year2}";
	}

	// Returns total number fully completed matches 
	public int GetCompletedCount()
	{
		int completed = 0;

		// Count total ended
		foreach ( Match match in Matches )
		{
			if ( match.IsEnded )
			{
				completed++;
			}
		}

		return completed;
	}

	// Builds 'won-lost-tied' record label
	public string GetRecord()
	{
		// 'wins-losses'
		string record = $"{Won}-{Lost}";

		// Only show ties if they exist
		if ( Tied > 0 )
		{
			record += $"-{Tied}";
		}

		return record;
	}

	/* Date Range */

	// Returns date of start of season (as defined by first started match)
	public DateTimeOffset GetStartDate()
	{
		List<Match> matches = Matches.OrderBy( m => m.Created ).ToList();

		// Find first started match
		foreach ( Match match in matches.Where( match => !match.IsNew ) )
		{
			if ( match.StartTime != null )
			{
				return (DateTimeOffset)match.StartTime;
			}
		}

		// No matches
		return DateTimeOffset.MinValue;
	}

	// Returns date of end of season (as defined by last started match)
	public DateTimeOffset GetEndDate()
	{
		List<Match> matches = Matches.OrderByDescending( m => m.Created ).ToList();

		// Find last started match
		foreach ( Match match in matches.Where( match => !match.IsNew ) )
		{
			if ( match.StartTime != null )
			{
				return (DateTimeOffset)match.StartTime;
			}
		}

		// No matches
		return DateTimeOffset.MaxValue;
	}

	/* Players */

	// Determines if roster contains specified player
	public bool HasPlayer( string uniqueId )
	{
		foreach ( Player player in Players )
		{
			if ( player.UniqueId == uniqueId )
			{
				return true;
			}
		}

		// No match
		return false;
	}

	// Returns root player matching specified unique key
	public Player GetRootPlayer( string key )
	{
		// Assume key is root ID
		Player player = Players.Find( p => p.RootId == key );

		// Not root, try season unique ID
		player ??= Players.Find( p => p.UniqueId == key );

		// Should always be one or other
		return player;
	}

	// Returns list of currently active roster players (sorted)
	public List<Player> GetActivePlayers()
	{
		return GetPlayers().Where( player => player.IsDeactivated == false ).ToList();
	}

	/* Sorted */

	// Returns list of roster Players in currently configured sort order
	public List<Player> GetPlayers()
	{
		string sort = Shell.Settings.GeneralSort;

		return sort switch
		{
			Settings.SortLastKey => Players.OrderBy( p => p.LastName ).ToList(),
			Settings.SortFirstKey => Players.OrderBy( p => p.FirstName ).ToList(),
			Settings.SortNumberKey => Players.OrderBy( p => DXUtils.ConvertToInt( p.Number ) ).ToList(),

			_ => Players,
		};
	}

	// Returns list of Coaches in default sort order
	public List<Coach> GetCoaches()
	{
		return Coaches.OrderBy( c => c.LastName ).ToList();
	}

	// Returns list of Statisticians in default sort order
	public List<Statistician> GetStatisticians()
	{
		return Statisticians.OrderBy( s => s.LastName ).ToList();
	}

	/* Season Home */

	// Returns specified number of players from roster
	public List<Player> GetHomePlayers( int count, bool shuffle )
	{
		List<Player> inList = [ ..Players ];
		List<Player> outList = new( count );

		// Optionally randomize
		if ( shuffle )
		{
			inList.Shuffle();
		}

		// Only add players that have photo
		outList.AddRange( inList.Where( player => player.HasPhoto ) );

		int empty = Math.Abs( count - outList.Count );

		// Leave empty slots blank
		for ( int i = 0; i < empty; i++ )
		{
			outList.Add( null );
		}

		return outList;
	}

	// Returns most recently used lineup for this season
	public Lineup GetHomeLineup()
	{
		Lineup lineup = null;

		int count = Lineups.Count;

		if ( count > 0 )
		{
			// Check recent matches first
			for ( int i = (Matches.Count - 1); i >= 0; i-- )
			{
				Match match = Matches[i];

				// Use lineup from first set
				if ( match.SetCount > 0 )
				{
					lineup = match.Sets[0].Lineup1;
				}
			}

			// Otherwise use last created
			lineup ??= Lineups[ count - 1 ];
		}

		// Lineup must have at least 1 photo
		return ((lineup != null) && lineup.HasPhoto()) ? lineup : null;
	}

	// Returns first head coach found for this season
	public Coach GetHomeCoach()
	{
		return Coaches.FirstOrDefault( coach => coach.Type.Equals( "head" ) );
	}

	// Returns a representative statistician for this season
	public Statistician GetHomeStatistician()
	{
		// Find most recently used
		for ( int i = (Matches.Count - 1); i >= 0; i-- )
		{
			Match match = Matches[i];

			if ( match.Statistician != null )
			{
				return match.Statistician;
			}
		}

		int count = Statisticians.Count;

		// Otherwise use last created or none
		return (count > 0) ? Statisticians[ count - 1 ] : null;
	}

	// Returns a representative opponent for this season
	public Opponent GetHomeOpponent()
	{
		// Find most recently used
		for ( int i = (Matches.Count - 1); i >= 0; i-- )
		{
			Match match = Matches[i];

			if ( match.Opponent != null )
			{
				return match.Opponent;
			}
		}

		int count = Opponents.Count;

		// Otherwise use last created or none
		return (count > 0) ? Opponents[ count - 1 ] : null;
	}

	// Returns a representative venue for this season
	public Venue GetHomeVenue()
	{
		// Find most recently used
		for ( int i = (Matches.Count - 1); i >= 0; i-- )
		{
			Match match = Matches[i];

			if ( match.Venue != null )
			{
				return match.Venue;
			}
		}

		int count = Venues.Count;

		// Otherwise use last created or none
		return (count > 0) ? Venues[0] : null;
	}

	/* Permissions */

	// User has permission to view all Seasons within accessible team
	public static IList<Season> CanView( Team team )
	{
		return team.Seasons;
	}

	// Determines if user has permission to create Seasons
	public static bool CanCreate( Team team, User user )
	{
		return user.Level switch
		{
			// Director/coach/stat can
			User.LevelType.Director or 
			User.LevelType.Coach or 
			User.LevelType.Statistician => !team.IsSample || user.IsAdmin,
			
			// No-one else can
			_ => false
		};
	}

	// Determines if user has permission to edit Seasons
	public static bool CanEdit( User user )
	{
		return user.Level switch
		{
			// Director/coach/stat always can
			User.LevelType.Director or 
			User.LevelType.Coach or 
			User.LevelType.Statistician => true,
			
			// No-one else can
			_ => false
		};
	}

	// Determines if user has permission to analyze Season stats
	public bool CanAnalyze( User user )
	{
		return user.Level switch
		{
			// Media cannot
			User.LevelType.Media => false,
			
			// Statistician only if linked to player
			User.LevelType.Statistician => user.HasPermission( this ),
			
			// Everyone else can
			_ => true
		};
	}

	/* Populate */

	// Forces a full resync of season data
	public async Task Resync()
	{
		// Resync season data
		await PopulateFull( true );

		// Recalc win-loss records
		await UpdateRecord();
	}

	// Populates all child objects
	public async Task PopulateFull( bool force = false )
	{
		if ( !IsPopulated || force )
		{
																														DXProfiler.Start();
			// Populate underlying data			
			await PopulateRoster( force );
			await PopulateMatches( force );
																														DXProfiler.Mark( "Populate" );
			// Read all children
			Lineups = await ReadChildren<Lineup>( Lineup.CollectionKey, "SeasonId" );
																														DXProfiler.Mark( "Lineups" );
			Tournaments = await ReadChildren<Tournament>( Tournament.CollectionKey, "SeasonId" );
			Sets = await ReadChildren<Set>( Set.CollectionKey, "SeasonId" );
																														DXProfiler.Mark( "Sets" );
			Coaches = await ReadChildren<Coach>( Coach.CollectionKey, "SeasonId" );
			Statisticians = await ReadChildren<Statistician>( Statistician.CollectionKey, "SeasonId" );
			Opponents = await ReadChildren<Opponent>( Opponent.CollectionKey, "SeasonId" );
			Venues = await ReadChildren<Venue>( Venue.CollectionKey, "SeasonId" );
																														DXProfiler.Mark( "Tags" );
			// Populate all parents
			foreach ( Player player in Players ) player.Season = this;
			foreach ( Lineup lineup in Lineups ) lineup.Season = this;

			foreach ( Tournament tournament in Tournaments ) tournament.Season = this;
			foreach ( Match match in Matches ) match.Season = this;

			foreach ( Coach coach in Coaches ) coach.Season = this;
			foreach ( Statistician statistician in Statisticians ) statistician.Season = this;
			foreach ( Opponent opponent in Opponents ) opponent.Season = this;
			foreach ( Venue venue in Venues ) venue.Season = this;

			// Some children require further (non db) population
			foreach ( Lineup lineup in Lineups ) lineup.Populate();
			foreach ( Match match in Matches ) match.Populate( this );
			foreach ( Tournament tournament in Tournaments ) tournament.Populate();
																														DXProfiler.Mark( "FKs" );
			IsPopulated = true;
		}
	}

	// Populates only roster
	public async Task PopulateRoster( bool force = false )
	{
		if ( (Players.Count == 0) || force )
		{
			Players = await ReadChildren<Player>( Player.CollectionKey, "SeasonId" );
		}
	}

	// Populates only matches
	public async Task PopulateMatches( bool force, bool deep = false )
	{
		if ( (Matches.Count == 0) || force )
		{
			Matches = await ReadChildren<Match>( Match.CollectionKey, "SeasonId" );

			// Optionally populate within each match
			if ( deep )
			{
				foreach ( Match match in Matches )
				{
					match.Populate( this );
				}
			}
		}
	}

	// Populates ONLY child objects required for season import
	public async Task PopulateImport()
	{
		if ( !IsPopulated )
		{
			// Populate children
			await PopulateRoster( true );

			Tournaments = await ReadChildren<Tournament>( Tournament.CollectionKey, "SeasonId" );
			Coaches = await ReadChildren<Coach>( Coach.CollectionKey, "SeasonId" );
			Statisticians = await ReadChildren<Statistician>( Statistician.CollectionKey, "SeasonId" );
			Opponents = await ReadChildren<Opponent>( Opponent.CollectionKey, "SeasonId" );
			Venues = await ReadChildren<Venue>( Venue.CollectionKey, "SeasonId" );

			// Populate parents
			foreach ( Player player in Players ) player.Season = this;
			foreach ( Tournament tournament in Tournaments ) tournament.Season = this;
			foreach ( Coach coach in Coaches ) coach.Season = this;
			foreach ( Statistician statistician in Statisticians ) statistician.Season = this;
			foreach ( Opponent opponent in Opponents ) opponent.Season = this;
			foreach ( Venue venue in Venues ) venue.Season = this;
		}
	}

	/* Find */

	// Returns pre-populated child object matching specified identifier
	public Player GetPlayer( string uniqueId ) { return (uniqueId == null) ? null : Players.Find( p => (p.UniqueId == uniqueId) || (p.RootId == uniqueId) ); }	// Also check Root
	public Lineup GetLineup( string uniqueId ) { return (uniqueId == null) ? null : Lineups.Find( l => l.UniqueId == uniqueId ); }

	public Tournament GetTournament( string uniqueId ) { return (uniqueId == null) ? null : Tournaments.Find( t => t.UniqueId == uniqueId ); }

	public Statistician GetStatistician( string uniqueId ) { return (uniqueId == null) ? null : Statisticians.Find( s => s.UniqueId == uniqueId ); }
	public Opponent GetOpponent( string uniqueId ) { return (uniqueId == null) ? null : Opponents.Find( o => o.UniqueId == uniqueId ); }
	public Venue GetVenue( string uniqueId ) { return (uniqueId == null) ? null : Venues.Find( v => v.UniqueId == uniqueId ); }

	// Returns pre-populated Matches within specified Tournament
	public List<Match> GetMatches( string uniqueId ) { return Matches.FindAll( m => m.TournamentId == uniqueId ); }
	public Match GetMatch( string uniqueId ) { return Matches.Find( m => m.UniqueId == uniqueId ); }

	// Returns child object matched by specified name (case insenstive)
	public Player GetPlayerByName( string name ) { return Players.Find( p => p.FullName.Equals( name, StringComparison.OrdinalIgnoreCase ) ); }
	public Lineup GetLineupByName( string name ) { return Lineups.Find( l => l.Name.Equals( name, StringComparison.OrdinalIgnoreCase ) ); }

	public Tournament GetTournamentByName( string name ) { return Tournaments.Find( t => t.Name.Equals( name, StringComparison.OrdinalIgnoreCase ) ); }

	public Statistician GetStatisticianByName( string name ) { return Statisticians.Find( s => s.FullName.Equals( name, StringComparison.OrdinalIgnoreCase ) ); }
	public Opponent GetOpponentByName( string name ) { return Opponents.Find( o => o.ShortName.Equals( name, StringComparison.OrdinalIgnoreCase ) ); }
	public Venue GetVenueByName( string name ) { return Venues.Find( v => v.Name.Equals( name, StringComparison.OrdinalIgnoreCase ) ); }

	/* Analysis */

	// Returns list of all matches (optionally excluding scrimmages)
	public List<Match> GetMatches( bool sort = true )
	{
		List<Match> matches = [];
	
		bool scrimmages = Shell.Settings.AnalyzeScrimmage;
	
		// Filter out scrimmages
		foreach ( Match match in Matches )
		{
			if ( scrimmages || !match.IsScrimmage )
			{
				matches.Add( match );
			}
		}
	
		// Optionally sort chronologically
		return sort ? matches.OrderByDescending( m => m.StartTime ).ToList() : matches;
	}

	// Filters season matches against specified filter list
	public List<Match> FilterMatches( TagFilter filter = null )
	{
		List<Match> matches = [];
		
		// Optionally filter on match tags
		matches.AddRange( Matches.Where( match => (filter == null) || filter.Filter( match ) ) );
		
		return matches;
	}

	// Aggregates all data for analyzing the scope of this Season
	public async Task<DataStats> Aggregate( TagFilter filter = null )
	{
		// Make sure all matches loaded
		await PopulateFull();
	
		DataStats stats = new();
	
		// Optionally apply match filter(s)
		List<Match> matches = (filter == null) ? GetMatches( false ) : FilterMatches( filter );
	
		// Build stats list from each match
		foreach ( Match match in matches )
		{
			stats.Add( await match.Aggregate() );
		}
	
		return stats;
	}
	
	// Aggregates all raw summary data for scope of this Season
	public async Task<StatSummary> AggregateRaw( TagFilter filter = null )
	{
		// Make sure all matches loaded
		await PopulateFull();
	
		StatSummary summary = new();
	
		// Optionally apply match filter(s)
		List<Match> matches = (filter == null) ? GetMatches( false ) : FilterMatches( filter );
	
		// Aggregate summary data for each match
		foreach ( Match match in matches )
		{
			summary.Add( match.AggregateRaw() );
		}
	
		return summary;
	}

	/* CRUD */

	// Reads and returns Season matching specified unique identifier
	public static async Task<Season> Read( string uniqueId )
	{
		return await Read<Season>( CollectionKey, uniqueId );
	}

	// Updates purchase consumed status for this season
	public async Task UpdateConsumed()
	{
		// Only needs to be done once
		if ( !Consumed.HasValue )
		{
			Consumed = true;

			await Update( "Consumed", Consumed );
		}
	}

	// Updates purchase allocated to this season
	public async Task UpdatePurchase( string purchaseId )
	{
		PurchaseId = purchaseId;

		await Update( "PurchaseId", PurchaseId );
	}

	// Updates won/lost/tied fields for this object
	public async Task UpdateRecord()
	{
		IWriteBatch batch = DXData.StartBatch();

		// Update within batch
		UpdateRecord( batch );

		await DXData.CommitBatch( batch );
	}

	// Updates won/lost/tied fields for this object (batched)
	public void UpdateRecord( IWriteBatch batch )
	{
		// MUST be pre-populated
		Won = 0; 
		Lost = 0; 
		Tied = 0;

		// Accumulate totals from all matches in season
		foreach ( Match match in Matches.Where( match => match.IsEnded && !match.IsScrimmage ) )
		{
			if ( match.Won )
			{
				Won++;
			}
			else if ( match.Lost )
			{
				Lost++;
			}
			else if ( match.Tied )
			{
				Tied++;
			}
		}

		// Persist
		Update( batch, "Won", Won );
		Update( batch, "Lost", Lost );
		Update( batch, "Tied", Tied );
	}

	// Performs cascading delete for this Season
	public override async Task Delete( bool remove = true )
	{
		User user = Shell.CurrentUser;
		
		// Remove from total if not used
		await PurchaseEngine.Deallocate( user, this );

		// Delete children
		foreach ( Player player in Players ) { await player.Delete(); }
		foreach ( Lineup lineup in Lineups ) { await lineup.Delete(); }

		foreach ( Match match in Matches ) { await match.Delete(); }
		foreach ( Tournament tournament in Tournaments ) { await tournament.Delete(); }

		foreach ( Coach coach in Coaches ) { await coach.Delete(); }
		foreach ( Statistician statistician in Statisticians ) { await statistician.Delete(); }
		foreach ( Opponent opponent in Opponents ) { await opponent.Delete(); }
		foreach ( Venue venue in Venues ) { await venue.Delete(); }

		// Remove from parent
		if ( remove )
		{
			Team.Seasons.Remove( this );
		}

		// Delete self
		await base.Delete( remove );
	}

    // Adding set into this season.
    public void AddToSet(Set set)
    {
	    Sets ??= [];
	    
	    Sets.Add(set);
    }
}

//
