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

using Plugin.Firebase.Firestore;

using DXLib.UI;
using DXLib.UI.Layout;
using DXLib.UI.Container;

using DXLib.UI.Control;
using DXLib.UI.Control.Button;

using DXLib.UI.Form;
using DXLib.UI.Form.Control;

using DXLib.Data;
using DXLib.Utils;

namespace iStatVball3;

/*
 * Provides admin tools for archiving raw stats from Firestore DB to a compressed, serialized file format stored in
 * Google Cloud Storage. Queries against Cloud Storage are faster and storage costs significantly less than Firestore.
 * */
public class AdminArchive : DXGridLayout
{
	/* Constants */
	private const int DefaultLimit = 10;

	/* Properties */
	private int Limit => limitNum.Number ?? DefaultLimit;
	private bool IsDebug => debugChk.Checked;

	/* Fields */
	private DXKeypadField limitNum;
	private DXCheckboxField debugChk;
	private DXButton archiveBtn;

	private DXLabel stats1Lbl;
	private DXLabel stats2Lbl;
	private DXLabel stats3Lbl;

	private DXScroll scrollView;
	private DXLabel logLbl;

	// Stats
	private DateTimeOffset startTime;

	private int totalSets;
	private int totalDone;
	private int totalDeleted;

	private int totalArchived;
	private int totalNew;
	private int totalProgress;

	private int totalElapsed;
	private int totalErrors;

	/* Methods */
	public AdminArchive()
	{
		BackgroundColor = DXColors.Light4;
		Padding = 22;

		RowSpacing = 10;
		ColumnSpacing = 10;

		Horizontal = LayoutOptions.Fill;
		Vertical = LayoutOptions.Fill;

		CreateControls();
	}

	// Adds all controls for configuring tool
	private void CreateControls()
	{
		// Limit
		limitNum = new DXKeypadField
		{
			Key = "limit",
			Title = "archive.limit",
			Number = DefaultLimit,
			MinValue = 1,
			MaxValue = 9999,
			Help = null,

			Horizontal = LayoutOptions.Start,
			Vertical = LayoutOptions.Center
		};

		limitNum.Init();
		limitNum.SetState( DXFormControl.ControlState.Normal );

		// Debug
		debugChk = new DXCheckboxField
		{
			Margin = 0,
			Padding = 0,
			
			Text = "archive.debug",

			Checked = true,
			HasLine = false,
			
			Horizontal = LayoutOptions.Fill,
			Vertical = LayoutOptions.Center
		};

		debugChk.Init();

		// Button
		archiveBtn = new DXButton
		{
			Resource = "archive.title",
			Type = DXButton.ButtonType.Neutral,

			Horizontal = LayoutOptions.End,
			Vertical = LayoutOptions.Center,

			IsDisabled = false,
			IsSticky = true,

			ButtonWd = 90,
			ButtonTapped = OnArchiveTapped
		};

		archiveBtn.Init();
		
		// Stats 1
		stats1Lbl = new DXLabel
		{
			Text = string.Empty,
			LineBreakMode = LineBreakMode.NoWrap,

			TextColor = DXColors.Dark1,
			Font = DXFonts.RobotoBold,
			FontSize = 14,

			Horizontal = LayoutOptions.Start,
			Vertical = LayoutOptions.Center
		};

		// Stats 2
		stats2Lbl = new DXLabel
		{
			Text = string.Empty,
			LineBreakMode = LineBreakMode.NoWrap,

			TextColor = DXColors.Dark1,
			Font = DXFonts.RobotoBold,
			FontSize = 14,

			Horizontal = LayoutOptions.Start,
			Vertical = LayoutOptions.Center
		};

		// Stats 3
		stats3Lbl = new DXLabel
		{
			Text = string.Empty,
			LineBreakMode = LineBreakMode.NoWrap,

			TextColor = DXColors.Dark1,
			Font = DXFonts.RobotoBold,
			FontSize = 14,

			Horizontal = LayoutOptions.Start,
			Vertical = LayoutOptions.Center
		};

		// Log scroller
		scrollView = new DXScroll
		{
			Padding = 0,
			Orientation = ScrollOrientation.Vertical,
			
			Horizontal = LayoutOptions.Fill,
			Vertical = LayoutOptions.Fill
		};

		// Log
		logLbl = new DXLabel
		{
			Text = string.Empty,
			LineBreakMode = LineBreakMode.WordWrap,

			TextColor = DXColors.Dark1,
			Font = DXFonts.Roboto,
			FontSize = 14,

			Horizontal = LayoutOptions.Fill,
			Vertical = LayoutOptions.Fill
		};

		scrollView.Content = logLbl;

		// 5 rows
		AddFixedRow( 60 );		// 0: limit, debug, button
		AddFixedRow( 14 );		// 1: stats 1
		AddFixedRow( 14 );		// 2: stats 2
		AddFixedRow( 14 );		// 3: stats 3
		AddStarRow();			// 4: scroll/log

		// 3 columns
		AddStarColumn( 25 );    // 0: limit
		AddStarColumn( 40 );	// 1: debug
		AddStarColumn( 35 );	// 2: button

		// Add components
		Add( limitNum, 0, 0 );
		Add( debugChk, 1, 0 );
		Add( archiveBtn, 2, 0 );

		Add( stats1Lbl, 0, 1, 3, 1 );
		Add( stats2Lbl, 0, 2, 3, 1 );
		Add( stats3Lbl, 0, 3, 3, 1 );

		Add( scrollView, 0, 4, 3, 1 );
	}

	/* Archive */

	// Starts archival process
	private async Task Archive()
	{
		ClearStats();
		ClearLog();

		// Query list of sets to be archived
		List<Set> sets = await QuerySets();

		if ( sets != null )
		{
			totalSets = sets.Count;
			UpdateStats();
																														Log( "STARTING" );
			// Process each set
			for ( int i = 0; i < totalSets; i++ )
			{
				DXProfiler.Start( );

				Set set = sets[i];

				// Archive
				if ( !set.IsArchived )
				{
					await ArchiveSet( set, i );
				}

				// Increment stats
				totalDone++;
				totalElapsed += (int)DXProfiler.Elapsed;

				UpdateStats();
			}
		}

		archiveBtn.Reset();
	}
	
	// Runs archival process for specified set
	private async Task ArchiveSet( Set set, int index )
	{
		try
		{ 
																														Log( "" ); Log( "SET: {0}", (index + 1) );
			bool error = false;
			bool eligible = true;

			// In-progress sets archived after 90 days
			if ( set.IsInProgress && (set.StartTime != null) )
			{
				totalProgress++;

				TimeSpan elapsed = DateTimeOffset.Now.Subtract( (DateTimeOffset)set.StartTime );
																														Log( "IN-PROGRESS: {0}", (int)elapsed.TotalDays );
				eligible = (elapsed.TotalDays > 90);
			}

			// OK to proceed
			if ( eligible )
			{
				TimeSpan elapsed = DateTimeOffset.Now.Subtract( set.Created );
																														if ( set.IsNew ) Log( "NEW: {0}", (int)elapsed.TotalDays );
				// Nothing to archive if set never started
				if ( set.IsNew )
				{
					totalNew++;
				}
				else
				{
																														Log( "{0}", set.UniqueId );
					// Check for stats in Firestore
					set.StatCache = await set.ReadStats();

					int dbCount = set.StatCache?.Count ?? -1;
					bool dbMiss = (dbCount < 1);
																														Log( "DB: {0}", dbCount );
					// Nothing to do if not in Firestore or empty (or from paper)
					if ( !dbMiss )
					{
						// Make sure exists in Cloud Storage
						SetData data = await SetData.ReadCloud( set.UniqueId, false );

						int cloudCount = data?.Count ?? -1;
						bool cloudMiss = (cloudCount < 1);

						bool verified = true;
																														Log( "CLOUD: {0}", cloudCount );
						// Not in Cloud Storage, must backup there first
						if ( cloudMiss )
						{
							bool success = await set.CacheCloud( false );

							if ( success )
							{
								totalArchived++;
																														Log( "ARCHIVED" );
								if ( IsDebug )
								{
									// Verify archived successfully
									SetData data2 = await SetData.ReadCloud( set.UniqueId, false );

									// Must equal original DB stat count
									int verifyCount = data2?.Count ?? -1;
									verified = (verifyCount == dbCount);

									if ( !verified )
									{
										error = true;
										totalErrors++;
									}

									// Log
																														if ( verified ) Log( "VERIFIED: {0}", verifyCount ); 
																														else Error( "VERIFY FAILED: {0} DB: {1} CLOUD:{2}", verifyCount, dbCount, cloudCount );
								}
							}
							else
							{
								error = true;
								totalErrors++;
																														Log( "ARCHIVE FAILED" );
							}
						}

						// Now OK to delete Firestore stats
						if ( verified )
						{
							await set.DeleteStats();

							// Mark as archived for debug
							await set.UpdateArchived();
																														Log( "DELETED" );
							// Verify deleted
							set.StatCache = await set.ReadStats();

							int deleteCount = set.StatCache?.Count ?? -1;
							bool deleted = (deleteCount < 1);
																														if ( deleted ) Log( "DELETE VERIFIED: {0}", deleteCount ); else Error( "DELETE FAILED: {0} {1}", deleteCount, dbCount );
							// Track deletions
							if ( deleted )
							{
								totalDeleted++;
							}
							else
							{
								error = true;
								totalErrors++;
							}
						}
					}
				}
			}

			// Unless error, mark as processed to avoid churning on new/in-progress
			if ( !error )
			{
				set.Deleted = DXUtils.Now();

				await set.Update( "Deleted", set.Deleted );
			}
																														Log( "UPDATED" );
		}
		// Error
		catch ( Exception ex )
		{
			totalErrors++;

			Error( "ERROR: {0}", ex.Message );
		}
	}

	// Queries for sets not already archived (within specified limit)
	private async Task<List<Set>> QuerySets()
	{
		ICollectionReference reference = DXData.Firestore.GetCollection( Set.CollectionKey );
		IQuery notArchived = reference.WhereEqualsTo( "Deleted", null );
		IQuery limited = notArchived.LimitedTo( Limit );
		IQuerySnapshot<Set> snapshot = await limited.GetDocumentsAsync<Set>();
		
		List<Set> list = [];
		
		// Build list
		list.AddRange( from document in snapshot.Documents select document.Data );
		
		return list;
	}

	/* Stats */

	// Resets accumulated values for all archival stats
	private void ClearStats()
	{
		startTime = DXUtils.Now();

		totalSets = 0;
		totalDone = 0;
		totalDeleted = 0;

		totalArchived = 0;
		totalNew = 0;
		totalProgress = 0;

		totalElapsed = 0;
		totalErrors = 0;

		UpdateStats();
	}

	// Updates display labels with current stat values
	private void UpdateStats()
	{
		// Total: Done: Deleted:
		string total = totalSets.ToString();
		string done = GetPercent( totalDone, totalSets );
		string deleted = GetPercent( totalDeleted );

		stats1Lbl.Text = DXString.Get( "archive.stats1", total, done, deleted );

		// Archived: New: Progress:
		string archived = GetPercent( totalArchived );
		string news = GetPercent( totalNew );
		string progress = GetPercent( totalProgress );

		stats2Lbl.Text = DXString.Get( "archive.stats2", archived, news, progress );

		// Elapsed: Avg: Errors:
		string elapsed = GetTime( DXUtils.ElapsedTime( startTime ), false );
		string average = GetTime( ((totalDone == 0) ? 0 : (totalElapsed / totalDone)), true );
		string errors = GetPercent( totalErrors );

		stats3Lbl.Text = DXString.Get( "archive.stats3", elapsed, average, errors );
	}

	// Formats specified value as percent of done 'X.X%'
	private string GetPercent( int value )
	{
		return GetPercent( value, totalDone );
	}

	// Formats specified value as percent of total 'X.X%'
	private static string GetPercent( int value, int total )
	{
		float percent = (total == 0) ? 0 : (value / (float)total);

		return $"{percent:P1}";
	}

	// Formats specified elapsed time as 'Xh Ym Zs'
	private static string GetTime( int millis, bool precise )
	{
		return DXUtils.FromDuration( millis, precise );
	}

	/* Log */

	// Logs specified error message (always shown)
	private void Error( string msg, params object[] values )
	{
		logLbl.Text += string.Format( msg, values );
		logLbl.Text += "\n";

		// Disable console for performance
		//DXLog.Error( msg, values );
	}

	// Logs specified message with default granularity level
	private void Log( string msg, params object[] values )
	{
		if ( IsDebug )
		{
			logLbl.Text += string.Format( msg, values );
			logLbl.Text += "\n";

			scrollView.ScrollToBottom( true );

			// Disable console for performance
			//DXLog.Debug( msg, values );
		}
	}

	// Clears all previous log output
	private void ClearLog()
	{
		logLbl.Text = string.Empty;
	}

	/* Callbacks */

	// Admin tapped Archive button
	private async void OnArchiveTapped( object sender )
	{
		await Archive();
	}
}

//
