﻿using System;
using System.Collections.Generic;
using System.Linq;
using Facepunch.Extend;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;

namespace Oxide.Plugins
{
    [Info("Note Logger", "un-boxing-man", "2.0.3")]
    [Description("Logs notes to a file.")]
    class NoteLogger : RustPlugin
    {
        const string permAdmin = "NoteLogger.admin";

        class NoteInfo
        {
            public string Text;
            public string DisplayName;
            public ulong UserID;
            public ItemId NoteID;
            public DateTime TimeStamp;

            public NoteInfo() { }

            public NoteInfo(BasePlayer player, string Text, ItemId NoteID)
            {
                this.Text = Text;
                this.DisplayName = player.displayName;
                this.UserID = player.userID;
                this.NoteID = NoteID;
                this.TimeStamp = DateTime.UtcNow;
            }
        }

        List<NoteInfo> NoteList = new List<NoteInfo>();
        // Perf: reuse lists for entity queries
        private readonly List<StorageContainer> _containerList = new List<StorageContainer>();
        private readonly List<BasePlayer> _playerList = new List<BasePlayer>();

        #region Oxide Hooks

        void Init()
        {
            permission.RegisterPermission(permAdmin, this);

            lang.RegisterMessages(new Dictionary<string, string>()
            {
                { "NoPermission", "You don't have permission to use this." },
                { "NoteInfo-Tp-Usage", "Incorrect usage! /noteinfo tp {{UID}}" },
                { "NoNoteFound", "Please hold the note in the first slot of your hotbar :)" },
                { "NoteInfoTitle", "Info for note {NoteID}:" },
                { "NoteInfoItem", "[{TimeStamp}] {Name} ({UserID}): {Text}" },
                { "NoteInfoNone", "No updates to show." },
                { "CouldntFindNote", "The note #{UID} could not be found." },
                { "TeleportedToNote", "You have been teleported to note #{UID}." },
                { "ConsoleLogMessage", "[{TimeStamp}] {Name} ({UserID}) ID {UID}: {Text}" },
                { "LogMessage", "[{TimeStamp}] {Name} ({UserID}) ID {UID}: {Text}" }
            }, this);

            ReadData();
        }

        void OnServerSave()
        {
            SaveData();
        }

        #endregion

        #region Config

        ConfigFile config;

        class ConfigFile
        {
            [JsonProperty(PropertyName = "Log Notes To Console")]
            public bool LogToConsole;

            public static ConfigFile DefaultConfig()
            {
                return new ConfigFile
                {
                    LogToConsole = true
                };
            }
        }

        protected override void LoadConfig()
        {
            base.LoadConfig();
            config = Config.ReadObject<ConfigFile>();
        }

        protected override void LoadDefaultConfig() => config = ConfigFile.DefaultConfig();

        protected override void SaveConfig() => Config.WriteObject(config);

        #endregion

        #region Commands

        [ChatCommand("note")]
        void NoteInfoCommand(BasePlayer player, string command, string[] args)
        {
            if (!HasPerm(player))
            {
                SendReply(player, GetMessage("NoPermission", player));
                return;
            }

            if (args != null && args.Length > 1 && args[0].ToLower() == "tp")
            {
                uint noteIDu;
                ulong noteIDl;
                ulong.TryParse(args[1], out noteIDl);


                if (!uint.TryParse(args[1], out noteIDu))
                {
                    SendReply(player, GetMessage("NoteInfo-Tp-Usage", player));
                    return;
                }
                ItemId noteID = new ItemId(noteIDl);

                Item foundItem = null;

                // Perf: Use Vis.Entities for containers
                _containerList.Clear();
                Vis.Entities<StorageContainer>(Vector3.zero, float.MaxValue, _containerList);
                foreach(var container in _containerList)
                {
                    if (container?.inventory == null) continue;
                    foundItem = container.inventory.FindItemByUID(noteID);
                    if (foundItem != null) break; // Found it
                }

                // If not found in containers, check player inventories
                if (foundItem == null)
                {
                    // Perf: Use Vis.Entities for players
                    _playerList.Clear();
                    Vis.Entities<BasePlayer>(Vector3.zero, float.MaxValue, _playerList);
                    foreach (var p in _playerList)
                    {
                        if (p?.inventory == null) continue;
                        foundItem = p.inventory.FindItemByUID(noteID);
                        if (foundItem != null) break; // Found it
                    }
                }

                if (foundItem == null)
                {
                    SendReply(player, GetMessage("CouldntFindNote", player).Replace("{UID}", noteID.ToString()));
                    return;
                }

                // Perf: Use the found item directly
                var rootContainer = foundItem.GetRootContainer();
                // Need to check if rootContainer is null (e.g., item is somehow detached)
                if (rootContainer == null)
                {
                    SendReply(player, GetMessage("CouldntFindNote", player).Replace("{UID}", noteID.ToString())); // Or a more specific error
                    return;
                }

                Vector3 targetPosition = Vector3.zero;
                bool positionFound = false;

                if (rootContainer.entityOwner != null) // Check if owned by an entity (e.g., StorageContainer)
                {
                    targetPosition = rootContainer.entityOwner.transform.position;
                    positionFound = true;
                }
                else if (rootContainer.playerOwner != null) // Check if owned by a player
                {
                    targetPosition = rootContainer.playerOwner.transform.position;
                    targetPosition += Vector3.up * 0.1f; // Offset slightly if in player inventory
                    positionFound = true;
                }

                if (!positionFound)
                {
                    // Could not determine the position of the note's container
                    SendReply(player, GetMessage("CouldntFindNote", player).Replace("{UID}", noteID.ToString())); // Consider a more specific error message
                    return;
                }

                Teleport(player, targetPosition);
                SendReply(player, GetMessage("TeleportedToNote", player).Replace("{UID}", noteID.ToString()));
                return;
            }

            var slotZero = player.inventory.containerBelt.GetSlot(0);

            if (slotZero == null || slotZero.info.displayName.english != "Note")
            {
                SendReply(player, GetMessage("NoNoteFound", player));
                return;
            }

            SendReply(player, GetMessage("NoteInfoTitle", player).Replace("{NoteID}", slotZero.uid.ToString()));
            var updateList = NoteList.Where(x => x.NoteID == slotZero.uid).OrderBy(x => x.TimeStamp).ToList();
            foreach (var update in updateList)
            {
                SendReply(player, GetMessage("NoteInfoItem", player)
                                    .Replace("{TimeStamp}", update.TimeStamp.ToString())
                                    .Replace("{Name}", update.DisplayName)
                                    .Replace("{UserID}", update.UserID.ToString())
                                    .Replace("{Text}", update.Text.TrimEnd('\n')));
            }
            if (updateList.Count == 0) SendReply(player, GetMessage("NoteInfoNone", player));
        }

        [ConsoleCommand("note.update")]
        void NoteUpdateCommand(ConsoleSystem.Arg arg)
        {
            uint num = arg.GetUInt(0, 0);
            string str = arg.GetString(1, string.Empty);
            Item item = arg.Player().inventory.FindItemByUID(new ItemId(num));
            if (item == null) return;
            item.text = str.Truncate(1024, null);
            item.MarkDirty();
            NoteList.Add(new NoteInfo(arg.Player(), item.text, item.uid));
            LogToConsole(arg.Player(), item.text, (uint)item.uid.Value);
            logTofile(arg.Player(), item.text, (uint)item.uid.Value);
        }

        #endregion

        #region Functions

        void LogToConsole(BasePlayer player, string text, uint uid)
        {
            if (!config.LogToConsole) return;
            Puts(GetMessage("ConsoleLogMessage", null)
                    .Replace("{TimeStamp}", DateTime.Now.ToString())
                    .Replace("{Name}", player.displayName)
                    .Replace("{UserID}", player.UserIDString)
                    .Replace("{UID}", uid.ToString())
                    .Replace("{Text}", text.TrimEnd('\n')));
        }
        void logTofile(BasePlayer player, string text, uint uid)
        {
            if (text == "") return;
            var filename = DateTime.Now.ToString("yyyy-MM-dd");
            var Message = (GetMessage("LogMessage", null)
                    .Replace("{TimeStamp}", DateTime.Now.ToString())
                    .Replace("{Name}", player.displayName)
                    .Replace("{UserID}", player.UserIDString)
                    .Replace("{UID}", uid.ToString())
                    .Replace("{Text}", text.TrimEnd('\n')));
            Puts(DateTime.UtcNow.ToString());
            LogToFile(filename, Message, this, false);

        }

        void Teleport(BasePlayer player, Vector3 position)
        {
            if (player.net?.connection != null)
                player.ClientRPCPlayer(null, player, "StartLoading");
            StartSleeping(player);
            player.MovePosition(position);
            if (player.net?.connection != null)
                player.ClientRPCPlayer(null, player, "ForcePositionTo", position);
            if (player.net?.connection != null)
                player.SetPlayerFlag(BasePlayer.PlayerFlags.ReceivingSnapshot, true);
            player.UpdateNetworkGroup();
            player.SendNetworkUpdateImmediate();
            if (player.net?.connection == null) return;
            try { player.ClearEntityQueue(null); } catch { }
            player.SendFullSnapshot();
        }

        void StartSleeping(BasePlayer player)
        {
            if (player.IsSleeping())
                return;
            player.SetPlayerFlag(BasePlayer.PlayerFlags.Sleeping, true);
            if (!BasePlayer.sleepingPlayerList.Contains(player))
                BasePlayer.sleepingPlayerList.Add(player);
            player.CancelInvoke("InventoryUpdate");
        }

        #endregion

        #region Helpers

        string GetMessage(string key, BasePlayer player) => lang.GetMessage(key, this, player?.UserIDString);

        bool HasPerm(BasePlayer player) => (player.IsAdmin || permission.UserHasPermission(player.UserIDString, permAdmin));

        void SaveData() => Interface.Oxide.DataFileSystem.WriteObject(this.Title, NoteList);
        void ReadData() => NoteList = Interface.Oxide.DataFileSystem.ReadObject<List<NoteInfo>>(this.Title);

        #endregion
    }
}