/*
 * 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 an individual volleyball match. Matches can either be contained within a tournament or directly within a
 * season.
 */
public class Match : DXImageModel
{
	/* Constants */
	public const string CollectionKey = "Matches";

	// Recording types
	public const string LiveKey = "live";
	public const string FutureKey = "future";
	public const string PaperKey = "paper";
	public const string VideoKey = "video";
	public const string RallySyncKey = "rallysync";

	// Match types
	public const string ScrimmageKey = "scrimmage";

	// End result (for Team 1)
	public enum MatchResult
	{
		Won,
		Lost,
		Tied
	};

	// Matches always 1-5 sets
	public const int MaxSets = 5;

	/* Properties */

	// Required
	[FirestoreProperty("Recording")] public string Recording { get; set; }

	[FirestoreProperty("MatchTime")] public DateTimeOffset MatchTime { get; set; }

	[FirestoreProperty("StartTime")] public DateTimeOffset? StartTime { get; set; }
	[FirestoreProperty("EndTime")] public DateTimeOffset? EndTime { get; set; }

	// Paper only
	[FirestoreProperty("PaperSetCount")] public int? PaperSetCount { get; set; }

	// Optional
	[FirestoreProperty("Type")] public string Type { get; set; }
	[FirestoreProperty("ScoreFormat")] public string ScoreFormat { get; set; }

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

	[FirestoreProperty("Attendance")] public int? Attendance { get; set; }

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

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

	// Results
	[FirestoreProperty("Result")] public int Result { get; set; }

	[FirestoreProperty("Sets1")] public int Sets1 { get; set; }
	[FirestoreProperty("Sets2")] public int Sets2 { get; set; }

	// Video settings (map)
	[FirestoreProperty("Video")] public MatchVideo Video { get; set; }

	// Set scores (arrays)
	[FirestoreProperty("Scores1")] public IList<int> Scores1 { get; set; }
	[FirestoreProperty("Scores2")] public IList<int> Scores2 { get; set; }

	// MaxPreps
	[FirestoreProperty("Exported")] public bool Exported { get; set; }

	// References (FK)
	[FirestoreProperty("StatisticianId")] public string StatisticianId { get; set; }
	[FirestoreProperty("OpponentId")] public string OpponentId { get; set; }
	[FirestoreProperty("VenueId")] public string VenueId { get; set; }

	// Parents (FK)
	[FirestoreProperty("SeasonId")] public string SeasonId { get; set; }
	[FirestoreProperty("TournamentId")] public string TournamentId { get; set; }

	/* Ignored */
	[JsonIgnore] public string RecordingName => DXString.GetLookupValue( "match.recording", Recording );
	[JsonIgnore] public string TypeName => DXString.GetLookupValue( "match.type", Type );

	// Display name for match
	[JsonIgnore] public string Name => GetName();
	[JsonIgnore] public string VsName => IsAnonOpponent ? Name : $"{DXString.Get( "match.opp.vs" )} {Opponent?.ShortName}";
	[JsonIgnore] public string Detail => IsAnonOpponent ? DXUtils.TimeFromDate( MatchTime ) : DXUtils.MonthFromDate( MatchTime, false );
	[JsonIgnore] public override string ObjectName => GetShortName();

	// Custom color
	[JsonIgnore] public Color Color => Season.Color;

	// State
	[JsonIgnore] public bool IsNew => StartTime == null;
	[JsonIgnore] public bool IsEnded => EndTime != null;
	[JsonIgnore] public bool IsInProgress => !IsNew && !IsEnded;

	[JsonIgnore] public bool IsLive => Recording == LiveKey;
	[JsonIgnore] public bool IsFuture => Recording == FutureKey;
	[JsonIgnore] public bool IsPaper => Recording == PaperKey;
	[JsonIgnore] public bool IsVideo => Recording == VideoKey;
	[JsonIgnore] public bool IsRallySync => Recording == RallySyncKey;

	[JsonIgnore] public bool IsScrimmage => Type is ScrimmageKey;
	[JsonIgnore] public bool IsSample => Season.IsSample;
	
	[JsonIgnore] public bool IsSynced => Video is { Sync: true };
	[JsonIgnore] public bool IsRallySynced => IsRallySync && IsSynced;
	
	[JsonIgnore] private MatchResult ResultType => (MatchResult)Result;

	[JsonIgnore] public bool Won => IsEnded && (Result == (byte)MatchResult.Won);
	[JsonIgnore] public bool Lost => IsEnded && (Result == (byte)MatchResult.Lost);
	[JsonIgnore] public bool Tied => IsEnded && (Result == (byte)MatchResult.Tied);

	[JsonIgnore] public int SetCount => Sets.Count;
	[JsonIgnore] public bool HasMaxSets => SetCount == MaxSets;

	[JsonIgnore] public DateTime? Date => StartTime?.DateTime ?? DateTime.MinValue;

	// Anonymous opponent? 
	[JsonIgnore] public bool IsAnonOpponent => OpponentId == null;

	// Team 1/2
	[JsonIgnore] public Team Team1 => Season.Team;
	[JsonIgnore] public Team Team2 => null;

	[JsonIgnore] public string Team1Id => Team1.UniqueId;
	[JsonIgnore] public string Team2Id => (Team2 == null) ? (IsAnonOpponent ? null : Opponent.UniqueId) : Team2.UniqueId;

	[JsonIgnore] public string Team1Name => Team1.Organization.Name;
	[JsonIgnore] public string Team2Name => (Team2 == null) ? (IsAnonOpponent ? DXString.Get( "match.opp" ) : Opponent?.Organization) : Team2.Organization.Name;

	[JsonIgnore] public string Team1Abbrev => Team1.AbbrevName;
	[JsonIgnore] public string Team2Abbrev => (Team2 == null) ? (IsAnonOpponent ? DXString.Get( "match.opp.abbrev" ) : Opponent.Abbreviation) : Team2.AbbrevName;

	[JsonIgnore] public Color Team1Color => ColorForTeam( Team1Id );
	[JsonIgnore] public Color Team2Color => ColorForTeam( Team2Id );

	// Child sets (eligible for JSON export for restore)
	[JsonIgnore] public IList<Set> Sets { get; set; }

	// References
	[JsonIgnore] public Statistician Statistician { get; set; }
	[JsonIgnore] public Opponent Opponent { get; set; }
	[JsonIgnore] public Venue Venue { get; set; }

	// Parent(s)
	[JsonIgnore] public Season Season { get; set; }
	[JsonIgnore] public Tournament Tournament { get; set; }

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

		// Allocate containers
		Scores1 = new List<int>( MaxSets );
		Scores2 = new List<int>( MaxSets );

		Sets = new List<Set>();
	}

	// Tests equality based on unique ID
	public override bool Equals( object obj )
	{
		return (obj is Match match) && match.UniqueId.Equals( UniqueId );
	}

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

	// Determines if specified team is primary team
	public bool IsTeam1( string teamId )
	{
		return (teamId == Team1Id);
	}

	// Determines if this match contains specified set
	public bool Contains( Set value )
	{
		return Enumerable.Contains( Sets, value );
	}

	// Returns last set in match, if any
	public Set GetLastSet()
	{
		int count = Sets.Count;

		return (count > 0) ? Sets[ count - 1 ] : null;
	}

	// Determines if last set in match is new or in-progress
	public bool SetInProgress()
	{
		Set lastSet = GetLastSet();

		return lastSet is { IsEnded: false };
	}

	// Returns players on court at time of first point of first set
	public async Task<IList<string>> GetStartingLineup()
	{
		return (Sets.Count > 0) ? await Sets[0].GetStartingPlayers() : null;
	}

	// Returns total duration of match, zero if NA
	public TimeSpan GetDuration()
	{
		if ( IsEnded && (StartTime != null) && (EndTime != null) )
		{
			return ((DateTimeOffset)EndTime).Subtract( (DateTimeOffset)StartTime );
		}
		
		return TimeSpan.Zero;
	}

	/* Life Cycle */

	// Ends this match in real-time
	public async Task End()
	{
		await End( DateTimeOffset.Now );
	}

	// Ends this match at specified time
	public async Task End( DateTimeOffset? time )
	{
		// Persist all match end data
		await UpdateEnd( time );

		// Season considered used once 1 match complete
		await Season.UpdateConsumed();

		// Prompt user to review app (once only, 30 day interval)
		await ReviewEngine.Prompt( this, true );
	}

	/* Display */

	// Returns match name based on opponent
	public string GetName()
	{
		// Anonymous opponent shows date
		if ( IsAnonOpponent || (Opponent == null) )
		{
			return DXUtils.MonthFromDate( MatchTime, false );
		}

		// Otherwise opponent name
		return GetOpponentName();
	}

	// Returns short format name for match based on opponent
	public string GetShortName()
	{
		string date = DXUtils.LabelFromDate( MatchTime, true );

		// No opponent, date only
		if ( IsAnonOpponent || (Opponent == null) )
		{
			return date;
		}

		// 'Foo 14-1 (3/31/21)'
		return $"{GetOpponentName()} ({date})";
	}

	// Returns opponent name as 'Foo Club 14-1' (club) or 'Foo HS' (school)
	public string GetOpponentName()
	{
		return (Opponent == null) ? null : (Season.Team.Organization.IsClub ? Opponent.FullName : Opponent.ShortName);
	}

	// Return abbreviated form of opponent name
	public string GetOpponentAbbrev()
	{
		return (Opponent == null) ? DXString.GetUpper( "opponent.anon" ) : Opponent.Abbreviation;
	}

	// Returns display label for team matching specified ID
	public string LabelForTeam( string teamId, bool abbrev = true )
	{
		// Team 2 (anonymous opponent)
		if ( teamId == null )
		{
			return abbrev ? Team2Abbrev : Team2Name;
		}

		// Team 1 (always first order)
		if ( teamId.Equals( Team1Id ) )
		{
			return abbrev ? Team1Abbrev : Team1Name;
		}

		// Team 2 (named or first order)
		return abbrev ? Team2Abbrev : Team2Name;
	}

	// Returns color for team matching specified ID
	public Color ColorForTeam( string teamId )
	{
		return IsTeam1( teamId ) ? Team1.Color : ((Opponent == null) ? Opponent.DefaultColor : Opponent.Color);
	}

	// Returns image to be used for this match (custom, opponent, or none)
	public string GetImageUrl()
	{
		return string.IsNullOrEmpty( ImageUrl ) ? Opponent?.ImageUrl : ImageUrl;
	}

	/* Result */

	// Determines match winner/loser based on sets won/lost
	private MatchResult CalcResult()
	{
		// Team1 won
		if ( Sets1 > Sets2 )
		{
			return MatchResult.Won;
		}

		// Team2 won
		if ( Sets2 > Sets1 )
		{
			return MatchResult.Lost;
		}

		// Tie
		return MatchResult.Tied;
	}

	// Returns string resource corresponding to match result
	public string GetResultRsrc()
	{
		return ResultType switch
		{
			MatchResult.Won => "match.won",
			MatchResult.Lost => "match.lost",
			MatchResult.Tied => "match.tied",

			_ => null,
		};
	}

	// Returns sets won/lost in format '3-1'
	public string GetSetResult()
    {
		return $"{Sets1}-{Sets2}";
	}

	// Refreshes scores from all underlying sets 
	public void RefreshSetScores()
	{
		Scores1.Clear();
		Scores2.Clear();

		// Re-populate all scores
		foreach ( Set set in Sets )
		{
			Scores1.Add( set.Points1 );
			Scores2.Add( set.Points2 );
		}
	}
	
	/* Permissions */

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

	// Determines if user has permission to edit Matches
	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 Match stats
	public static bool CanAnalyze()
	{
		// Everyone can (BoxScore)
		return true;
	}

	/* Analysis */

	// Aggregates all data for analyzing scope of this Match
	public async Task<DataStats> Aggregate( bool force = false )
	{
		DataStats stats = new();

		// Build stats list from each set in match
		foreach ( Set set in Sets )
		{
			stats.Add( await set.Aggregate( force ) );
		}

		return stats;
	}

	// Aggregates all raw summary data for scope of this Match
	public StatSummary AggregateRaw()
	{
		StatSummary summary = new();

		// Aggregate summary data for each set in match
		foreach ( Set set in Sets )
		{
			StatSummary setSummary = set.AggregateRaw();

			// Set may not have summary data
			if ( setSummary != null )
			{
				summary.Add( setSummary );
			}
		}

		return summary;
	}

	/* Populate */

	// Populates all reference objects (no db)
	public void Populate( Season season, bool sets = true )
	{
		Season = season;
		Tournament = Season.GetTournament( TournamentId );

		Statistician = Season.GetStatistician( StatisticianId );
		Opponent = Season.GetOpponent( OpponentId );
		Venue = Season.GetVenue( VenueId );

		// Optionally also populate all sets (no db query)
		if ( sets )
		{
			if ( Season.Sets is { Count: > 0 } )
			{
				Sets = Season.Sets.Where( s => s.MatchId == UniqueId ).OrderBy( s => s.Number ).ToList();
			}

			foreach ( Set set in Sets )
			{
				set.Populate( this );
			}
		}
	}

	/* Cache */

	// Writes this Match to local cache for restore support
	public void CacheLocal()
	{
		string key = $"match_{UniqueId}";

		// Write to MonkeyBarrel
		DXData.WriteCache( key, this );
	}

	/* CRUD */

	// UPDATE

	// Updates Match start time (batched)
	public void UpdateStart( IWriteBatch batch, DateTimeOffset? start )
	{
		StartTime = start;

		// No longer Future Match once started
		if ( IsFuture )
		{
			Recording = LiveKey;

			Update( batch, "Recording", Recording );
		}

		// Persist
		Update( batch, "StartTime", StartTime );
	}

	// Updates Match end time (determines in-progress/ended status)
	private async Task UpdateEnd( DateTimeOffset? end )
	{
		EndTime = end;

		// Cache again locally first for safety
		CacheLocal();

		// Then write to Firestore (batched)
		IWriteBatch batch = DXData.StartBatch();

		// End match
		Update( batch, "EndTime", EndTime );

		// Update scores, result, record
		UpdateScores( batch );
		UpdateResult( batch );
		UpdateRecord( batch );

		// Persist
		await DXData.CommitBatch( batch );
	}

	// Renumbers sets 1..N following a set deletion (batched)
	public void UpdateSetNumbers( IWriteBatch batch )
	{
		for ( int i = 0; i < SetCount; i++ )
		{
			Set set = Sets[i];
			byte number = (byte)(i + 1);

			set.UpdateNumber( batch, number );
		}
	}

	// Updates all match result related data (internally batched)
	public async Task UpdateAllResults()
	{
		IWriteBatch batch = DXData.StartBatch();

		UpdateScores( batch );
		UpdateResult( batch );
		UpdateRecord( batch );

		await DXData.CommitBatch( batch );
	}

	// Updates scores for all sets in match (batched)
	public void UpdateScores( IWriteBatch batch )
	{
		RefreshSetScores();
		
		Update( batch, "Scores1", Scores1 );
		Update( batch, "Scores2", Scores2 );
	}

	// Updates sets won/lost at end set or following delete (batched)
	public void UpdateResult( IWriteBatch batch )
	{
		byte won = 0;
		byte lost = 0;

		// Accumulate wins/losses
		foreach ( Set set in Sets )
		{
			// Ended sets only
			if ( set.IsEnded )
			{
				switch ( (Set.SetResult)set.Result )
				{
					case Set.SetResult.Won: won++; break;
					case Set.SetResult.Lost: lost++; break;
				}
			}
		}

		Sets1 = won;
		Sets2 = lost;

		// (Re)calculate result
		Result = (byte)CalcResult();

		// Persist
		Update( batch, "Sets1", Sets1 );
		Update( batch, "Sets2", Sets2 );
		Update( batch, "Result", Result );
	}

	// Updates win/loss record in all related objects
	public async Task UpdateRecord()
	{
		IWriteBatch batch = DXData.StartBatch();

		// Batched update
		UpdateRecord( batch );

		await DXData.CommitBatch( batch );
	}

	// Updates win/loss record in all related objects (batched)
	public void UpdateRecord( IWriteBatch batch )
	{
		// Exclude scrimmages
		if ( !IsScrimmage )
		{
			// Update all match tags
			Tournament?.UpdateRecord( batch );
			Opponent?.UpdateRecord( batch );
			Statistician?.UpdateRecord( batch );
			Venue?.UpdateRecord( batch );

			// Update parent season
			Season.UpdateRecord( batch );
		}
	}

	// Updates opposing team for this match
	public async Task UpdateOpponent( Opponent opponent )
	{
		string oldOpponentId = OpponentId;

		// Update each set and all underlying stats (no db)
		foreach ( Set set in Sets )
		{
			await set.UpdateOpponent( opponent );
		}

		// Change match (MUST be last)
		OpponentId = opponent?.UniqueId;
		Opponent = opponent;

		IWriteBatch batch = DXData.StartBatch();

		// Must update old opponent record
		Season.GetOpponent( oldOpponentId )?.UpdateRecord( batch );

		await DXData.CommitBatch( batch );
	}

	// Updates MaxPreps export status for this match
	public async Task UpdateExported()
	{
		Exported = true;

		await Update( "Exported", Exported );
	}

	// DELETE

	// Performs cascading delete on this Match
	public override async Task Delete( bool remove = true )
	{
		// Delete children
		foreach ( Set set in Sets )
		{
			await set.Delete( false );
		}

		// Remove from parent(s)
		if ( remove )
		{
			Tournament?.Matches.Remove( this );

			Season.Matches.Remove( this );
		}

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

//
