using System;
using System.Linq;
using System.Management.Automation;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Runtime.InteropServices;
using System.Security;
using System.DirectoryServices;
using System.Diagnostics;
using System.Threading;
using System.Management.Automation.Runspaces;
using System.Security.Principal;
using C4IT.Logging;
using static C4IT.Logging.cLogManager;

namespace C4IT.LIAM
{
    public partial class ExchangeManager
    {
        private static readonly TimeSpan PowerShellInvokeTimeout = TimeSpan.FromSeconds(120);

        private static PSDataCollection<PSObject> InvokePowerShellWithTimeout(PowerShell ps, TimeSpan timeout, string operationName)
        {
            IAsyncResult asyncResult = null;
            try
            {
                asyncResult = ps.BeginInvoke();
                if (!asyncResult.AsyncWaitHandle.WaitOne(timeout))
                {
                    try
                    {
                        ps.Stop();
                    }
                    catch (Exception stopEx)
                    {
                        LogException(stopEx);
                    }

                    throw new TimeoutException(
                        $"PowerShell operation '{operationName}' timed out after {timeout.TotalSeconds:0} seconds.");
                }

                var results = ps.EndInvoke(asyncResult);
                if (ps.HadErrors)
                {
                    var errorMessage = CollectPowerShellErrors(ps);
                    throw new InvalidOperationException(
                        string.IsNullOrWhiteSpace(errorMessage)
                            ? $"PowerShell operation '{operationName}' failed without detailed error output."
                            : $"PowerShell operation '{operationName}' failed: {errorMessage}");
                }

                return results;
            }
            finally
            {
                if (asyncResult != null)
                    asyncResult.AsyncWaitHandle.Close();
            }
        }

        private static string CollectPowerShellErrors(PowerShell ps)
        {
            if (ps?.Streams?.Error == null || ps.Streams.Error.Count <= 0)
                return string.Empty;

            var errors = ps.Streams.Error
                .Select(e => e.Exception?.Message ?? e.ToString())
                .Where(m => !string.IsNullOrWhiteSpace(m))
                .Take(3);

            return string.Join(" | ", errors);
        }

        private static string GetSharedMailboxCreateErrorCode(Exception ex)
        {
            return ex is TimeoutException ? "EXCH_SHAREDMAILBOX_TIMEOUT" : "EXCH_SHAREDMAILBOX_CREATE_FAILED";
        }

        private static string GetDistributionGroupCreateErrorCode(Exception ex)
        {
            return ex is TimeoutException ? "EXCH_DISTRIBUTIONGROUP_TIMEOUT" : "EXCH_DISTRIBUTIONGROUP_CREATE_FAILED";
        }

        /// <summary>
        /// Stellt sicher, dass eine AD-Sicherheitsgruppe für den angegebenen AccessRole existiert (erstellt sie falls nicht)
        /// und wartet optional, bis die Replikation abgeschlossen ist.
        /// Liefert den tatsächlichen Gruppennamen zurück.
        /// </summary>
        private string EnsureSecurityGroup(eLiamAccessRoles accessRole, string baseName)
        {
            const int MaxLoop = 50; // Abbruchbedingung: nach 50 Versuchen abbrechen

            // 1. Namenskonvention für diese Rolle finden
            var namingConvention = _provider.NamingConventions
                .FirstOrDefault(nc => nc.AccessRole == accessRole);
            if (namingConvention == null)
                throw new InvalidOperationException($"Keine Namenskonvention für Rolle '{accessRole}' gefunden.");

            // 2. Benötigte CustomTags aus dem Provider ziehen
            //    - Prefix (z.B. "ACL")
            //    - GROUPTYPEPOSTFIX (z.B. "ExchangeMLMember")
            _provider.CustomTags.TryGetValue("ADGroupPrefix", out var prefix);
            _provider.CustomTags.TryGetValue(accessRole.ToString(), out var typePostfix);

            // 3. Schleife für _LOOP hochzählen, bis ein einzigartiger Name gefunden ist
            string groupName = null;
            string description = null;

            for (int loop = 0; loop <= MaxLoop; loop++)
            {
                // nur einfügen, wenn loop > 0
                var loopPart = loop > 0 ? $"_{loop}" : string.Empty;

                // Platzhalter im Template ersetzen
                groupName = namingConvention.NamingTemplate
                    .Replace("{{ADGroupPrefix}}", prefix ?? string.Empty)
                    .Replace("{{NAME}}", baseName)
                    .Replace("{{_LOOP}}", loopPart)
                    .Replace("{{GROUPTYPEPOSTFIX}}", typePostfix ?? string.Empty);

                description = namingConvention.DescriptionTemplate
                    .Replace("{{ADGroupPrefix}}", prefix ?? string.Empty)
                    .Replace("{{NAME}}", baseName)
                    .Replace("{{_LOOP}}", loopPart)
                    .Replace("{{GROUPTYPEPOSTFIX}}", typePostfix ?? string.Empty);

                // Existenz prüfen
                bool exists = GetSecurityGroups(groupName)
                    .Any(g => string.Equals(
                        g.Properties["sAMAccountName"]?.Value?.ToString(),
                        groupName,
                        StringComparison.OrdinalIgnoreCase));

                if (!exists)
                    break; // Name ist frei – raus aus der Schleife

                if (loop == MaxLoop)
                    throw new InvalidOperationException(
                        $"Konnte nach {MaxLoop} Versuchen keinen eindeutigen Gruppennamen für '{baseName}' erzeugen.");
            }
            // 4. Gruppen-Scope-Bit setzen
            int scopeBit;
            switch (namingConvention.Scope)
            {
                case eLiamAccessRoleScopes.Global:
                    scopeBit = 0x2;
                    break;
                case eLiamAccessRoleScopes.DomainLocal:
                    scopeBit = 0x4;
                    break;
                case eLiamAccessRoleScopes.Universal:
                    scopeBit = 0x8;
                    break;
                default:
                    scopeBit = 0x8;
                    break;
            }
            int groupType = unchecked((int)(0x80000000 | scopeBit));

            // 5. Gruppe im AD anlegen
            string ldapPath = $"LDAP://{_organizationalUnit}";
            string password = new System.Net.NetworkCredential(string.Empty, _credential.Password).Password;
            using (var root = new DirectoryEntry(
                       ldapPath,
                       _credential.UserName,
                       password,
                       AuthenticationTypes.Secure))
            {
                var newGroup = root.Children.Add($"CN={groupName}", "group");
                newGroup.Properties["sAMAccountName"].Value = groupName;
                newGroup.Properties["displayName"].Value = groupName;
                newGroup.Properties["groupType"].Value = groupType;
                if(!string.IsNullOrEmpty(description))
                {
                    newGroup.Properties["description"].Value = description;
                }
                
                newGroup.CommitChanges();
            }

            // 6. Auf Replikation warten (optional)
            const int replicationTimeoutMinutes = 2;
            if (!WaitForGroupReplication(groupName, TimeSpan.FromMinutes(replicationTimeoutMinutes)))
            {
                throw new TimeoutException(
                    $"Die AD-Gruppe '{groupName}' konnte innerhalb von {replicationTimeoutMinutes} Minuten nicht repliziert werden.");
            }

            return groupName;
        }

        /// <summary>
        /// Wartet darauf, dass die Gruppe nach der Erstellung im AD repliziert ist.
        /// </summary>
        private bool WaitForGroupReplication(string groupName, TimeSpan timeout)
        {
            var sw = Stopwatch.StartNew();
            var pollInterval = TimeSpan.FromSeconds(5);

            while (sw.Elapsed < timeout)
            {
                var found = GetSecurityGroups(groupName)
                    .Any(g => string.Equals(
                        g.Properties["sAMAccountName"]?.Value?.ToString(),
                        groupName,
                        StringComparison.OrdinalIgnoreCase));

                if (found) return true;
                Thread.Sleep(pollInterval);
            }

            return false;
        }

        /// <summary>
        /// Setzt das ManagedBy-Attribut einer AD-Gruppe auf eine andere Gruppe
        /// – mit den im Konstruktor übergebenen Credentials und Domain.
        /// </summary>
        private void SetManagedBy(string groupName, string managerGroup)
        {
            string ldapPath = $"LDAP://{_organizationalUnit}";

            // SecureString -> Klartext
            string password = SecureStringToString(_credential.Password);

            using (var root = new DirectoryEntry(ldapPath,
                                                 _credential.UserName,
                                                 password,
                                                 AuthenticationTypes.Secure))
            using (var ds = new DirectorySearcher(root))
            {
                // Gruppe holen
                ds.Filter = $"(&(objectClass=group)(sAMAccountName={groupName}))";
                var result = ds.FindOne();
                if (result == null)
                    throw new InvalidOperationException($"Gruppe '{groupName}' nicht gefunden in {ldapPath}");

                var groupEntry = result.GetDirectoryEntry();

                // DistinguishedName der Manager-Gruppe ermitteln
                using (var mgrSearch = new DirectorySearcher(root))
                {
                    mgrSearch.Filter = $"(&(objectClass=group)(sAMAccountName={managerGroup}))";
                    var mgrResult = mgrSearch.FindOne();
                    if (mgrResult == null)
                        throw new InvalidOperationException($"Manager-Gruppe '{managerGroup}' nicht gefunden in {ldapPath}");

                    string managerDn = mgrResult.GetDirectoryEntry()
                                                .Properties["distinguishedName"]
                                                .Value
                                                .ToString();

                    // Attribut setzen und speichern
                    groupEntry.Properties["managedBy"].Value = managerDn;
                    groupEntry.CommitChanges();
                }
            }
        }

        /// <summary>
        /// Erstellt eine Shared Mailbox samt zugehöriger AD-Gruppen (FullAccess, SendAs, Owner) und setzt die nötigen Berechtigungen.
        /// </summary>
        public Tuple<Guid, List<Tuple<string, string, string, string>>> CreateSharedMailboxWithOwnershipGroups(
            string name,
            string alias,
            string displayName = null,
            string primarySmtpAddress = null)
        {
            string errorCode;
            string errorMessage;

            var result = CreateSharedMailboxWithOwnershipGroups(
                name,
                alias,
                displayName,
                primarySmtpAddress,
                out errorCode,
                out errorMessage);

            if (result == null)
                throw new InvalidOperationException($"[{errorCode}] {errorMessage}");

            return result;
        }

        /// <summary>
        /// Erstellt eine Shared Mailbox samt zugehöriger AD-Gruppen (FullAccess, SendAs, Owner) und setzt die nötigen Berechtigungen.
        /// Liefert bei Fehlern einen Error-Code und eine Message zurück.
        /// </summary>
        public Tuple<Guid, List<Tuple<string, string, string, string>>> CreateSharedMailboxWithOwnershipGroups(
            string name,
            string alias,
            string displayName,
            string primarySmtpAddress,
            out string errorCode,
            out string errorMessage)
        {
            errorCode = string.Empty;
            errorMessage = string.Empty;
            CreationResult result = new CreationResult();

            try
            {
                LogEntry(
                    $"Start shared mailbox creation: Name='{name}', Alias='{alias}', DisplayName='{displayName}', PrimarySmtpAddress='{primarySmtpAddress}'",
                    LogLevels.Info);

                // Ensure AD groups
                string fullAccessGroup = EnsureSecurityGroup(eLiamAccessRoles.ExchangeSMBFullAccess, name);
                string sendAsGroup = EnsureSecurityGroup(eLiamAccessRoles.ExchangeSMBSendAs, name);
                string ownerGroup = EnsureSecurityGroup(eLiamAccessRoles.ExchangeSMBOwner, name);

                SetManagedBy(fullAccessGroup, ownerGroup);
                SetManagedBy(sendAsGroup, ownerGroup);

                LogEntry(
                    $"Shared mailbox groups prepared: FullAccess='{fullAccessGroup}', SendAs='{sendAsGroup}', Owner='{ownerGroup}'",
                    LogLevels.Debug);

                // Create mailbox and permissions
                using (Runspace rs = CreateRunspace())
                using (PowerShell ps = PowerShell.Create())
                {
                    ps.Runspace = rs;

                    ps.AddCommand("New-Mailbox");
                    ps.AddParameter("Name", name);
                    ps.AddParameter("Alias", alias);
                    ps.AddParameter("Shared", true);
                    ps.AddParameter("OrganizationalUnit", _organizationalUnit);
                    if (!string.IsNullOrEmpty(displayName))
                        ps.AddParameter("DisplayName", displayName);
                    if (!string.IsNullOrEmpty(primarySmtpAddress))
                        ps.AddParameter("PrimarySmtpAddress", primarySmtpAddress);

                    InvokePowerShellWithTimeout(ps, PowerShellInvokeTimeout, $"New-Mailbox '{alias}'");

                    ps.Commands.Clear();
                    ps.AddCommand("Add-MailboxPermission");
                    ps.AddParameter("Identity", name);
                    ps.AddParameter("User", fullAccessGroup);
                    ps.AddParameter("AccessRights", "FullAccess");
                    ps.AddParameter("InheritanceType", "All");
                    ps.AddParameter("ErrorAction", "Stop");
                    InvokePowerShellWithTimeout(ps, PowerShellInvokeTimeout, $"Add-MailboxPermission '{name}' -> '{fullAccessGroup}'");

                    ps.Commands.Clear();
                    ps.AddCommand("Add-ADPermission");
                    ps.AddParameter("Identity", name);
                    ps.AddParameter("User", sendAsGroup);
                    ps.AddParameter("ExtendedRights", "Send-As");
                    ps.AddParameter("AccessRights", "ExtendedRight");
                    ps.AddParameter("ErrorAction", "Stop");
                    InvokePowerShellWithTimeout(ps, PowerShellInvokeTimeout, $"Add-ADPermission Send-As '{name}' -> '{sendAsGroup}'");
                }

                // Retrieve mailbox GUID
                DirectoryEntry mbEntry = FindAdObject("(&(objectClass=user)(mailNickname=" + alias + "))");
                if (mbEntry != null && mbEntry.Properties.Contains("objectGUID") && mbEntry.Properties["objectGUID"].Count > 0)
                {
                    byte[] bytes = (byte[])mbEntry.Properties["objectGUID"][0];
                    result.ObjectGuid = new Guid(bytes);
                }

                // Collect group details
                string[] roles = new string[] {
                    eLiamAccessRoles.ExchangeSMBFullAccess.ToString(),
                    eLiamAccessRoles.ExchangeSMBSendAs.ToString(),
                    eLiamAccessRoles.ExchangeSMBOwner.ToString()
                };
                string[] names = new string[] {
                    fullAccessGroup,
                    sendAsGroup,
                    ownerGroup
                };

                for (int i = 0; i < roles.Length; i++)
                {
                    DirectoryEntry grpEntry = FindAdObject("(&(objectCategory=group)(sAMAccountName=" + names[i] + "))");
                    if (grpEntry != null && grpEntry.Properties.Contains("objectSid") && grpEntry.Properties["objectSid"].Count > 0)
                    {
                        byte[] sidBytes = (byte[])grpEntry.Properties["objectSid"][0];
                        string sid = new SecurityIdentifier(sidBytes, 0).Value;
                        string distinguishedName = grpEntry.Properties["distinguishedName"][0].ToString();
                        result.Groups.Add(Tuple.Create(roles[i], sid, names[i], distinguishedName));
                    }
                }

                errorCode = "OK";
                LogEntry(
                    $"Shared mailbox created successfully: Name='{name}', Alias='{alias}', ObjectGuid='{result.ObjectGuid}', GroupCount='{result.Groups.Count}'",
                    LogLevels.Info);

                return Tuple.Create(result.ObjectGuid, result.Groups);
            }
            catch (Exception ex)
            {
                errorCode = GetSharedMailboxCreateErrorCode(ex);
                errorMessage = ex.Message;
                LogEntry($"Shared mailbox creation failed [{errorCode}] {errorMessage}", LogLevels.Error);
                LogException(ex);
                return null;
            }
        }

        /// <summary>
        /// Erstellt eine Distribution Group samt zugehöriger AD-Gruppen (Member, Owner) und setzt die nötigen Berechtigungen.
        /// </summary>
        public Tuple<Guid, List<Tuple<string, string, string, string>>> CreateDistributionGroupWithOwnershipGroups(
           string name,
           string alias,
           string displayName = null,
           string primarySmtpAddress = null)
        {
            string errorCode;
            string errorMessage;

            var result = CreateDistributionGroupWithOwnershipGroups(
                name,
                alias,
                displayName,
                primarySmtpAddress,
                out errorCode,
                out errorMessage);

            if (result == null)
                throw new InvalidOperationException($"[{errorCode}] {errorMessage}");

            return result;
        }

        /// <summary>
        /// Erstellt eine Distribution Group samt zugehöriger AD-Gruppen (Member, Owner) und setzt die nötigen Berechtigungen.
        /// Liefert bei Fehlern einen Error-Code und eine Message zurück.
        /// </summary>
        public Tuple<Guid, List<Tuple<string, string, string, string>>> CreateDistributionGroupWithOwnershipGroups(
           string name,
           string alias,
           string displayName,
           string primarySmtpAddress,
           out string errorCode,
           out string errorMessage)
        {
            errorCode = string.Empty;
            errorMessage = string.Empty;
            CreationResult result = new CreationResult();

            try
            {
                LogEntry(
                    $"Start distribution group creation: Name='{name}', Alias='{alias}', DisplayName='{displayName}', PrimarySmtpAddress='{primarySmtpAddress}'",
                    LogLevels.Info);

                // Ensure AD groups
                string memberGroup = EnsureSecurityGroup(eLiamAccessRoles.ExchangeMLMember, name);
                string ownerGroup = EnsureSecurityGroup(eLiamAccessRoles.ExchangeMLOwner, name);

                SetManagedBy(memberGroup, ownerGroup);
                LogEntry(
                    $"Distribution group permission groups prepared: Member='{memberGroup}', Owner='{ownerGroup}'",
                    LogLevels.Debug);

                // Create distribution group and permissions
                using (Runspace rs = CreateRunspace())
                using (PowerShell ps = PowerShell.Create())
                {
                    ps.Runspace = rs;
                    ps.AddCommand("New-DistributionGroup");
                    ps.AddParameter("Name", name);
                    ps.AddParameter("Alias", alias);
                    ps.AddParameter("OrganizationalUnit", _organizationalUnit);
                    if (!string.IsNullOrEmpty(displayName))
                        ps.AddParameter("DisplayName", displayName);
                    if (!string.IsNullOrEmpty(primarySmtpAddress))
                        ps.AddParameter("PrimarySmtpAddress", primarySmtpAddress);
                    InvokePowerShellWithTimeout(ps, PowerShellInvokeTimeout, $"New-DistributionGroup '{alias}'");

                    // GUID holen
                    ps.Commands.Clear();
                    ps.AddCommand("Get-DistributionGroup")
                      .AddParameter("Identity", name);
                    var dg = InvokePowerShellWithTimeout(ps, PowerShellInvokeTimeout, $"Get-DistributionGroup '{name}'")
                        .FirstOrDefault();
                    if (dg != null && dg.Properties["Guid"] != null)
                    {
                        var guidVal = dg.Properties["Guid"].Value;
                        if (guidVal is Guid g) result.ObjectGuid = g;
                        else if (guidVal is string s && Guid.TryParse(s, out Guid parsed)) result.ObjectGuid = parsed;
                    }

                    ps.Commands.Clear();
                    ps.AddCommand("Add-DistributionGroupMember")
                      .AddParameter("Identity", name)
                      .AddParameter("Member", memberGroup)
                      .AddParameter("ErrorAction", "Stop");
                    InvokePowerShellWithTimeout(ps, PowerShellInvokeTimeout, $"Add-DistributionGroupMember '{name}' -> '{memberGroup}'");

                    ps.Commands.Clear();
                    ps.AddCommand("Set-DistributionGroup")
                      .AddParameter("Identity", name)
                      .AddParameter("ManagedBy", ownerGroup)
                      .AddParameter("ErrorAction", "Stop");
                    InvokePowerShellWithTimeout(ps, PowerShellInvokeTimeout, $"Set-DistributionGroup ManagedBy '{name}' -> '{ownerGroup}'");
                }

                // Collect group details
                string[] dRoles = new string[] {
                    eLiamAccessRoles.ExchangeMLMember.ToString(),
                    eLiamAccessRoles.ExchangeMLOwner.ToString()
                };
                string[] dNames = new string[] {
                    memberGroup,
                    ownerGroup
                };

                for (int i = 0; i < dRoles.Length; i++)
                {
                    DirectoryEntry grpEntry = FindAdObject("(&(objectCategory=group)(sAMAccountName=" + dNames[i] + "))");
                    if (grpEntry != null && grpEntry.Properties.Contains("objectSid") && grpEntry.Properties["objectSid"].Count > 0)
                    {
                        byte[] sidBytes = (byte[])grpEntry.Properties["objectSid"][0];
                        string sid = new SecurityIdentifier(sidBytes, 0).Value;
                        string distinguishedName = grpEntry.Properties["distinguishedName"][0].ToString();
                        result.Groups.Add(Tuple.Create(dRoles[i], sid, dNames[i], distinguishedName));
                    }
                }

                errorCode = "OK";
                LogEntry(
                    $"Distribution group created successfully: Name='{name}', Alias='{alias}', ObjectGuid='{result.ObjectGuid}', GroupCount='{result.Groups.Count}'",
                    LogLevels.Info);

                return Tuple.Create(result.ObjectGuid, result.Groups);
            }
            catch (Exception ex)
            {
                errorCode = GetDistributionGroupCreateErrorCode(ex);
                errorMessage = ex.Message;
                LogEntry($"Distribution group creation failed [{errorCode}] {errorMessage}", LogLevels.Error);
                LogException(ex);
                return null;
            }
        }
    

        /// <summary>
        /// Setzt das ManagedBy-Attribut einer Distribution Group.
        /// </summary>
        private void SetDistributionGroupManagedBy(string groupName, string managerGroup)
        {
            using (var runspace = CreateRunspace())
            using (var ps = PowerShell.Create())
            {
                ps.Runspace = runspace;
                ps.AddCommand("Set-DistributionGroup")
                  .AddParameter("Identity", groupName)
                  .AddParameter("ManagedBy", managerGroup)
                  .AddParameter("ErrorAction", "SilentlyContinue");
                ps.Invoke();
            }
        }

        /// <summary>
        /// Hilfsmethode: SecureString in Klartext wandeln.
        /// </summary>
        private static string SecureStringToString(SecureString ss)
        {
            if (ss == null) return string.Empty;
            IntPtr ptr = Marshal.SecureStringToBSTR(ss);
            try
            {
                return Marshal.PtrToStringBSTR(ptr) ?? string.Empty;
            }
            finally
            {
                Marshal.ZeroFreeBSTR(ptr);
            }
        }
    }
}
