using System;
using System.Collections.Generic;
using System.Linq;
using EDPlayerJournal.Entries;

namespace EDPlayerJournal.BGS;

public class Report {
    private List<Objective> objectives = new List<Objective>();

    public delegate void OnLogDelegate(string log);

    public event OnLogDelegate? OnLog;

    public List<Objective> Objectives {
        get { return objectives; }
        set { objectives = value; }
    }

    public bool AddObjective(Objective objective) {
        var found = objectives.Find(x => x.CompareTo(objective) == 0);
        bool added = false;

        if (found == null) {
            objectives.Add(objective);
            added = true;
        }

        return added;
    }

    public static bool IsRelevant(Entry e) {
        return e.Is(Events.CommitCrime) ||
            e.Is(Events.Docked) ||
            e.Is(Events.FactionKillBond) ||
            e.Is(Events.FSDJump) ||
            e.Is(Events.Location) ||
            e.Is(Events.MarketBuy) ||
            e.Is(Events.MarketSell) ||
            e.Is(Events.MissionAccepted) ||
            e.Is(Events.MissionFailed) ||
            e.Is(Events.MultiSellExplorationData) ||
            e.Is(Events.RedeemVoucher) ||
            e.Is(Events.SearchAndRescue) ||
            e.Is(Events.SellExplorationData) ||
            e.Is(Events.SellMicroResources) ||
            e.Is(Events.SellOrganicData) ||
            e.Is(Events.ShipTargeted) ||
            e.Is(Events.MissionCompleted)
            ;
    }

    public void Scan(PlayerJournal journal, DateTime start, DateTime end) {
        /* Log files only get rotated if you restart the game client. This means that there might
         * be - say - entries from the 4th of May in the file with a timestamp of 3rd of May. This
         * happens if you happen to play a session late into the night.
         * At first I tried extracting the first and last line of a file to see the date range, but
         * if you have a lot of files this becomes quite slow, and quite the memory hog (as journal
         * files have to be read in their entirety to check this). So we assume that you can't play
         * three days straight, and keep the code fast.
         */
        DateTime actualstart = start.AddDays(-3);
        List<Entry> entries = journal.Files
            .Where(f => f.NormalisedDateTime >= actualstart && f.NormalisedDateTime <= end)
            .SelectMany(e => e.Entries)
            .ToList()
            ;
        // Now further sort the list down to entries that are actually within the given datetime
        // Note that entry datetimes are not normalised, so we have to sort until end + 1 day
        DateTime actualend = end.AddDays(1);

        entries = entries
            .Where(e => e.Timestamp >= start && e.Timestamp < actualend)
            .ToList()
            ;
        Scan(entries);
    }

    public void Scan(List<Entry> entries) {
        if (entries.Count <= 0) {
            return;
        }

        List<Entry> relevant = entries
            .Where(x => IsRelevant(x))
            .ToList()
            ;

        Dictionary<ulong, MissionAcceptedEntry> acceptedMissions = new Dictionary<ulong, MissionAcceptedEntry>();
        Dictionary<string, long> buyCost = new Dictionary<string, long>();
        Dictionary<ulong, string> systems = new Dictionary<ulong, string>();
        Dictionary<string, string> npcfactions = new Dictionary<string, string>();
        Dictionary<string, List<Faction>> system_factions = new Dictionary<string, List<Faction>>();

        // A dictionary resolving to a station at which each mission was accepted
        Dictionary<ulong, string> acceptedStations = new Dictionary<ulong, string>();
        // A dictionary resolving to a system at which each mission was accepted
        Dictionary<ulong, ulong> acceptedSystems = new Dictionary<ulong, ulong>();

        string? current_system = null;
        ulong? current_system_address = null;
        string? current_station = null;
        string? controlling_faction = null;

        objectives.ForEach(x => x.Clear());

        foreach (Entry e in relevant) {
            List<Transaction> results = new List<Transaction>();
            bool collate = false;

            if (e.Is(Events.Docked)) {
                DockedEntry? docked = e as DockedEntry;
                if (docked == null) {
                    continue;
                }

                /* gleem the current station from this message
                 */
                current_station = docked.StationName;
                current_system = docked.StarSystem;
                controlling_faction = docked.StationFaction;
                current_system_address = docked.SystemAddress;

                if (current_system_address != null && current_system != null) {
                    if (!systems.ContainsKey(current_system_address.Value)) {
                        systems.Add(current_system_address.Value, current_system);
                    }
                }
            } else if (e.Is(Events.FSDJump)) {
                /* Gleem current system and controlling faction from this message.
                 */
                FSDJumpEntry? fsd = e as FSDJumpEntry;

                if (fsd == null || fsd.StarSystem == null) {
                    continue;
                }

                current_system_address = fsd.SystemAddress;
                current_system = fsd.StarSystem;
                controlling_faction = fsd.SystemFaction;

                if (!systems.ContainsKey(fsd.SystemAddress)) {
                    systems.Add(fsd.SystemAddress, fsd.StarSystem);
                }

                if (!system_factions.ContainsKey(fsd.StarSystem) &&
                    fsd.SystemFactions.Count > 0) {
                    system_factions[fsd.StarSystem] = fsd.SystemFactions;
                }
            } else if (e.Is(Events.Location)) {
                /* Get current system, faction name and station from Location message
                 */
                LocationEntry? location = e as LocationEntry;

                if (location == null || location.StarSystem == null) {
                    continue;
                }

                current_system = location.StarSystem;
                current_system_address = location.SystemAddress;

                if (!systems.ContainsKey(location.SystemAddress)) {
                    systems.Add(location.SystemAddress, location.StarSystem);
                }

                if (!string.IsNullOrEmpty(location.SystemFaction)) {
                    controlling_faction = location.SystemFaction;
                }
                if (!string.IsNullOrEmpty(location.StationName)) {
                    current_station = location.StationName;
                }

                if (!system_factions.ContainsKey(location.StarSystem) &&
                    location.SystemFactions.Count > 0) {
                    system_factions[location.StarSystem] = location.SystemFactions;
                }
            } else if (e.Is(Events.ShipTargeted)) {
                ShipTargetedEntry? targeted = e as ShipTargetedEntry;

                if (targeted == null) {
                    continue;
                }

                if (string.IsNullOrEmpty(targeted.PilotNameLocalised) ||
                    string.IsNullOrEmpty(targeted.Faction)) {
                    continue;
                }

                npcfactions[targeted.PilotNameLocalised] = targeted.Faction;
            } else if (e.Is(Events.CommitCrime)) {
                CommitCrimeEntry? crime = e as CommitCrimeEntry;
                if (crime == null) {
                    continue;
                }

                string? faction = crime.Faction;

                if (faction == null || !crime.IsMurder) {
                    /* we don't care about anything but murder for now */
                    continue;
                }

                /* use localised victim name if we have it, otherwise use normal name */
                string? victim = crime.VictimLocalised;
                if (string.IsNullOrEmpty(victim)) {
                    victim = crime.Victim;
                }

                if (string.IsNullOrEmpty(victim)) {
                    continue;
                }

                if (!npcfactions.ContainsKey(victim)) {
                    /* The faction in the crime report is the faction that issues the bounty,
                     * and not the faction of the victim.
                     */
                    OnLog?.Invoke(string.Format(
                        "No faction found for victim \"{0}\", using faction that issued the bounty instead.",
                        victim, crime.Faction
                        ));
                } else {
                    faction = npcfactions[victim];
                }

                results.Add(new FoulMurder(crime) {
                    System = current_system,
                    Faction = faction,
                });
                collate = true;
            } else if (e.Is(Events.MissionCompleted)) {
                MissionCompletedEntry? completed = e as MissionCompletedEntry;

                if (completed == null ||
                    completed.Mission == null) {
                    continue;
                }

                MissionAcceptedEntry? accepted = null;
                MissionCompleted? main_mission = null;
                ulong accepted_address;
                string? accepted_system;

                string? target_faction_name = completed.Mission.TargetFaction;
                string? source_faction_name = completed.Mission.Faction;

                if (!acceptedMissions.TryGetValue(completed.Mission.MissionID, out accepted)) {
                    OnLog?.Invoke(string.Format(
                        "Unable to find mission acceptance for mission \"{0}\". " +
                        "Please extend range to include the mission acceptance.", completed.Mission.LocalisedName
                        ));
                    continue;
                }

                if (!acceptedSystems.TryGetValue(completed.Mission.MissionID, out accepted_address)) {
                    OnLog?.Invoke(string.Format(
                        "Unable to figure out in which system mission \"{0}\" was accepted.", completed.Mission.LocalisedName
                        ));
                    continue;
                }

                if (!systems.TryGetValue(accepted_address, out accepted_system)) {
                    OnLog?.Invoke(string.Format(
                        "Unable to figure out in which system mission \"{0}\" was accepted.", completed.Mission.LocalisedName
                        ));
                    continue;
                }

                foreach (var other in completed.Mission.Influences) {
                    string faction = other.Key;
                    if (string.IsNullOrEmpty(faction)) {
                        OnLog?.Invoke(string.Format(
                            "Mission \"{0}\" has empty faction name in influence block, "+
                            "so this influence support was ignored. " +
                            "Please check the README on why this happens.", completed.Mission.LocalisedName)
                            );
                        continue;
                    }

                    /* Now comes the fun part. Sometimes the influence list is empty for a faction.
                     * This happens if the faction in question 
                     */
                    if (other.Value.Count() == 0) {
                        OnLog?.Invoke(string.Format(
                            "Mission \"{0}\" gave no influence to \"{1}\", so we assume this is because the " +
                            "faction is in a conflict and cannot gain influence right now. " +
                            "If this assessment is wrong, just remove the entry from the objective list.",
                            completed.Mission.LocalisedName, faction
                            ));

                        if (string.Compare(target_faction_name, faction, true) == 0) {
                            /* here we assume that if the faction in question is the target faction,
                             * that we gave said target faction no influence in the target system, aka
                             * current system
                             */
                            if (current_system_address == null) {
                                continue;
                            }
                            other.Value.Add(current_system_address.Value, "");
                            OnLog?.Invoke(string.Format(
                                "Mission \"{0}\" gave no influence to \"{1}\". Since \"{1}\" is the target faction " +
                                "of the mission, we assume the influence was gained in \"{2}\". " +
                                "Please remove the entry if this assumption is wrong.",
                                completed.Mission.LocalisedName, faction, current_system
                                ));
                        } else if (string.Compare(source_faction_name, faction, true) == 0) {
                            /* source faction of the mission is not getting any influence. This could be because
                             * the source faction is in an election state in its home system and cannot gain any
                             * influence. It may also very well be that the source and target faction are the same
                             * since the faction is present in both target and source system. In which case we add
                             * both and hope for the best.
                             */
                            other.Value.Add(accepted_address, "");
                            OnLog?.Invoke(string.Format(
                                "Mission \"{0}\" gave no influence to \"{1}\". Since \"{1}\" is the source faction " +
                                "of the mission, we assume the influence was gained in \"{2}\". " +
                                "Please remove the entry if this assumption is wrong.",
                                completed.Mission.LocalisedName, faction, accepted_system
                                ));

                            /* check if source/target faction are equal, in which case we also need an entry
                             * for the target system. As said factions can be present in two systems, and can
                             * give missions that target each other.
                             */
                            if (string.Compare(source_faction_name, target_faction_name, true) == 0 &&
                                current_system_address != null) {
                                other.Value.Add(current_system_address.Value, "");
                                OnLog?.Invoke(string.Format(
                                    "Mission \"{0}\" gave no influence to \"{1}\". Since \"{1}\" is the source and target faction " +
                                    "of the mission, we assume the influence was also gained in target system \"{2}\". " +
                                    "Please remove the entry if this assumption is wrong.",
                                    completed.Mission.LocalisedName, faction, current_system
                                    ));
                            }
                        }
                    }

                    foreach (var influences in other.Value) {
                        ulong system_address = influences.Key;
                        string? system;
                        string? accepted_station;

                        if (completed.Mission == null) {
                            continue;
                        }

                        if (!systems.TryGetValue(system_address, out system)) {
                            OnLog?.Invoke(string.Format(
                                "Unknown system \"{0}\" unable to assign that mission a target.", system_address
                                ));
                            continue;
                        }

                        if (!acceptedStations.TryGetValue(completed.Mission.MissionID, out accepted_station)) {
                            OnLog?.Invoke(string.Format(
                                "Unable to figure out in which station mission \"{0}\" was accepted.", completed.Mission.LocalisedName
                                ));
                            continue;
                        }

                        if (faction.Equals(source_faction_name) && system_address == accepted_address) {
                            /* This is the influence block for the origin of the mission.
                             */
                            main_mission = new MissionCompleted() {
                                CompletedEntry = completed,
                                AcceptedEntry = accepted,
                                System = accepted_system,
                                Faction = source_faction_name,
                                SystemAddress = accepted_address,
                                Station = accepted_station,
                            };
                            results.Add(main_mission);
                        } else if (!faction.Equals(source_faction_name) ||
                            (faction.Equals(source_faction_name) && system_address != accepted_address)) {
                            /* This block is for secondary factions (first if), or if the secondary faction
                             * is the same as the mission giver, but in another system (second if).
                             */
                            results.Add(new InfluenceSupport() {
                                Faction = faction,
                                Influence = influences.Value,
                                System = system,
                                SystemAddress = system_address,
                                RelevantMission = completed
                            });
                        }
                    }
                }
            } else if (e.Is(Events.MissionAccepted)) {
                MissionAcceptedEntry? accepted = e as MissionAcceptedEntry;

                if (accepted == null ||
                    current_station == null ||
                    current_system_address == null) {
                    continue;
                }

                if (accepted.Mission == null) {
                    continue;
                }

                ulong id = accepted.Mission.MissionID;

                if (!acceptedMissions.ContainsKey(id)) {
                    acceptedMissions[id] = accepted;
                }
                if (!acceptedStations.ContainsKey(id)) {
                    acceptedStations[id] = current_station;
                }
                if (!acceptedSystems.ContainsKey(id)) {
                    acceptedSystems[id] = current_system_address.Value;
                }
            } else if (e.Is(Events.MissionFailed)) {
                MissionFailedEntry? failed = e as MissionFailedEntry;
                MissionAcceptedEntry? accepted = null;
                ulong accepted_address = 0;
                string? accepted_system;
                string? accepted_station;

                if (failed == null || failed.Mission == null) {
                    continue;
                }

                if (!acceptedMissions.TryGetValue(failed.Mission.MissionID, out accepted)) {
                    OnLog?.Invoke("A mission failed which wasn't accepted in the given time frame. " +
                        "Please adjust start date to when the mission was accepted to include it in the list.");
                    continue;
                }

                if (accepted.Mission == null) {
                    continue;
                }

                if (!acceptedSystems.TryGetValue(failed.Mission.MissionID, out accepted_address)) {
                    OnLog?.Invoke(string.Format(
                        "Unable to figure out in which system mission \"{0}\" was accepted.", accepted.Mission.Name
                        ));
                    continue;
                }

                if (!systems.TryGetValue(accepted_address, out accepted_system)) {
                    OnLog?.Invoke(string.Format(
                        "Unable to figure out in which system mission \"{0}\" was accepted.", accepted.Mission.Name
                        ));
                    continue;
                }

                if (!acceptedStations.TryGetValue(failed.Mission.MissionID, out accepted_station)) {
                    OnLog?.Invoke(string.Format(
                        "Unable to figure out in which station mission \"{0}\" was accepted.", accepted.Mission.Name
                        ));
                    continue;
                }

                results.Add(new MissionFailed(accepted) {
                    Failed = failed, 
                    System = accepted_system,
                    Station = accepted_station,
                    Faction = accepted.Mission.Faction,
                    SystemAddress = accepted_address,
                });

                /* Mission failed should be collated if they are in the same system/station
                 */
                collate = true;
            } else if (e.Is(Events.SellExplorationData)) {
                SellExplorationDataEntry? explo = e as SellExplorationDataEntry;

                if (explo == null) {
                    continue;
                }

                results.Add(new Cartographics(explo) {
                    System = current_system,
                    Station = current_station,
                    Faction = controlling_faction,
                });

                /* colate single cartographic selling into one */
                collate = true;
            } else if (e.Is(Events.SellOrganicData)) {
                SellOrganicDataEntry? organic = e as SellOrganicDataEntry;

                if (organic == null) {
                    return;
                }

                /* organic data sold to Vista Genomics */
                results.Add(new OrganicData(organic) {
                    System = current_system,
                    Station = current_station,
                    Faction = controlling_faction,
                });

                collate = true;
            } else if (e.Is(Events.MultiSellExplorationData)) {
                MultiSellExplorationDataEntry? explo = e as MultiSellExplorationDataEntry;

                if (explo == null) {
                    continue;
                }

                /* For multi-sell-exploraton-data only the controlling faction of the station sold to matters.
                 */
                results.Add(new Cartographics(explo) {
                    System = current_system,
                    Station = current_station,
                    Faction = controlling_faction
                });

                collate = true;
            } else if (e.Is(Events.RedeemVoucher)) {
                RedeemVoucherEntry? voucher = e as RedeemVoucherEntry;
                List<Faction> current_factions = new List<Faction>();

                if (voucher == null || current_system == null) {
                    continue;
                }

                if (system_factions.ContainsKey(current_system)) {
                    current_factions = system_factions[current_system];
                } else {
                    OnLog?.Invoke("There are no current system factions, so turned in vouchers were ignored.");
                    continue;
                }

                foreach (string faction in voucher.Factions) {
                    if (current_factions.Find(x => x.Name == faction) == null) {
                        OnLog?.Invoke(
                            string.Format("Vouchers for \"{0}\" were ignored in \"{1}\" since said " +
                            "faction is not present there.", faction, current_system)
                            );
                        continue; /* faction is not present, so it is ignored */
                    }

                    /* Same for selling combat vouchers. Only the current controlling faction matters here.
                     */
                    results.Add(new Vouchers(voucher) {
                        System = current_system,
                        Station = current_station,
                        Faction = faction,
                        ControllingFaction = controlling_faction,
                    });
                }

                collate = true;
            } else if (e.Is(Events.SellMicroResources)) {
                SellMicroResourcesEntry? smr = e as SellMicroResourcesEntry;

                if (smr == null) {
                    continue;
                }

                results.Add(new SellMicroResources(smr) {
                    Faction = controlling_faction,
                    Station = current_station,
                    System = current_system
                });
            } else if (e.Is(Events.MarketBuy)) {
                MarketBuyEntry? buy = e as MarketBuyEntry;

                if (buy == null) {
                    continue;
                }

                if (string.IsNullOrEmpty(buy.Type) || buy.BuyPrice == 0) {
                    continue;
                }
                buyCost[buy.Type] = buy.BuyPrice;

                results.Add(new BuyCargo(buy) {
                    Faction = controlling_faction,
                    Station = current_station,
                    System = current_system,
                });

                collate = true;
            } else if (e.Is(Events.SearchAndRescue)) {
                SearchAndRescueEntry? sar = e as SearchAndRescueEntry;

                if (sar == null) {
                    continue;
                }

                results.Add(new SearchAndRescue(sar) {
                    Faction = controlling_faction,
                    Station = current_station,
                    System = current_system,
                });

                collate = true;
            } else if (e.Is(Events.MarketSell)) {
                MarketSellEntry? sell = e as MarketSellEntry;
                long profit = 0;

                if (sell == null || sell.Type == null) {
                    continue;
                }

                if (!buyCost.ContainsKey(sell.Type)) {
                    OnLog?.Invoke("Could not find buy order for the given commodity. Please adjust profit manually.");
                } else {
                    long avg = buyCost[sell.Type];
                    profit = (long)sell.TotalSale - (avg * sell.Count);
                }

                results.Add(new SellCargo(sell) {
                    Faction = controlling_faction,
                    Station = current_station,
                    System = current_system,
                    Profit = profit
                });
            }

            if (results == null || results.Count <= 0) {
                continue;
            }

            foreach (Transaction entry in results) {
                /* Find all objectives that generally match.
                 */
                var matches = objectives
                    .Where(x => x.Matches(entry) >= 2)
                    .OrderBy(x => x.Matches(entry))
                    ;

                Objective? objective = null;
                if (matches != null && matches.Count() > 0) {
                    /* Then select the one that matches the most.
                     */
                    objective = matches
                        .OrderBy(x => x.Matches(entry))
                        .Reverse()
                        .First()
                        ;
                } else {
                    /* create a new objective if we don't have one */
                    objective = new Objective() {
                        Station = (entry.Station ?? ""),
                        Faction = (entry.Faction ?? ""),
                        System = (entry.System ?? ""),
                    };
                    objectives.Add(objective);
                }

                Transaction? existing = null;

                existing = objective.LogEntries.Find(x => {
                    try {
                        return x.CompareTo(entry) == 0;
                    } catch (NotImplementedException) {
                        return false;
                    }
                });

                if (collate && existing != null) {
                    existing.Entries.Add(e);
                } else if (!collate || existing == null) {
                    objective.LogEntries.Add(entry);
                }
            }
        }
    }

    public void Scan(PlayerJournal journal) {
        Scan(journal, DateTime.Now, DateTime.Now);
    }
}