using System;
using System.DirectoryServices;
using System.Linq;
using System.Threading;
using C4IT.Logging;
using C4IT.LIAM;
using LiamAD;
using System.Collections.Generic;
using System.Security.Principal;
using System.Text;

namespace LiamAD
{
    /// <summary>
    /// Helfer für cLiamProviderAD: Erstellt AD Member- und Owner-Gruppen für Services
    /// nach konfigurierter Namenskonvention und setzt ManagedBy.
    /// </summary>
    public class ADServiceGroupCreator
    {
        private readonly cLiamProviderAD _provider;
        private readonly cActiveDirectoryBase _adBase;
        private readonly string _user;
        private readonly string _password;
        public enum ADGroupType
        {
            Security,     // Sicherheit
            Distribution  // Verteiler
        }
        public ADServiceGroupCreator(cLiamProviderAD provider)
        {
            _provider = provider ?? throw new ArgumentNullException(nameof(provider));
            _adBase = provider.activeDirectoryBase;
            _user = provider.Credential.Identification;
            _password = new System.Net.NetworkCredential(_user, provider.Credential.Secret).Password;
        }

        private string GetLdapServer()
        {
            return string.IsNullOrWhiteSpace(_adBase.EffectiveDomainController) ? _provider.Domain : _adBase.EffectiveDomainController;
        }

        private string GetLdapRoot()
        {
            return $"LDAP://{GetLdapServer()}/{_provider.GroupPath}";
        }

        private string GetLdapDomainRoot()
        {
            return $"LDAP://{GetLdapServer()}";
        }

        /// <summary>
        /// Erstellt oder findet beide AD-Gruppen (Member & Owner) für einen Service.
        /// Neu mit: gruppenbereich (Scope) und gruppentyp (für Member-Gruppe).
        /// Owner-Gruppe ist immer Security.
        /// </summary>
        public List<Tuple<string, string, string, string>> EnsureServiceGroups(
            string serviceName,
            string description = null,
            eLiamAccessRoleScopes gruppenbereich = eLiamAccessRoleScopes.Universal,
            ADGroupType gruppentyp = ADGroupType.Distribution,
            IEnumerable<string> ownerSidList = null,
            IEnumerable<string> memberSidList = null)
        {
            const int MaxLoop = 50;
            var result = new List<Tuple<string, string, string, string>>();

            // Konventionen für Member und Owner
            var ownerConv = _provider.NamingConventions
                .FirstOrDefault(nc => nc.AccessRole == eLiamAccessRoles.ADOwner);
            var memberConv = _provider.NamingConventions
                .FirstOrDefault(nc => nc.AccessRole == eLiamAccessRoles.ADMember);
            if (ownerConv == null || memberConv == null)
                throw new InvalidOperationException("Namenskonvention für ADMember oder ADOwner fehlt.");

            // Tags
            _provider.CustomTags.TryGetValue("ADGroupPrefix", out var prefix);
            _provider.CustomTags.TryGetValue("ADOwner", out var ownerPostfix);
            _provider.CustomTags.TryGetValue("ADMember", out var memberPostfix);

            // 1) Owner-Gruppe (immer Security)
            string ownerName = null;
            for (int loop = 0; loop <= MaxLoop; loop++)
            {
                string loopPart = loop > 0 ? "_" + loop : string.Empty;
                ownerName = ownerConv.NamingTemplate
                    .Replace("{{ADGroupPrefix}}", prefix ?? string.Empty)
                    .Replace("{{NAME}}", serviceName)
                    .Replace("{{_LOOP}}", loopPart)
                    .Replace("{{GROUPTYPEPOSTFIX}}", ownerPostfix);
                if (!GroupExists(ownerName)) break;
                if (loop == MaxLoop) throw new InvalidOperationException($"Kein eindeutiger Owner-Name für '{serviceName}' nach {MaxLoop} Versuchen.");
            }
            EnsureGroup(ownerName, ownerConv, description, managedByDn: null, gruppenbereich, ADGroupType.Security);
            AddMembersBySid(ownerName, ownerSidList);      // NEU: SIDs als Owner hinzufügen
            var ownerDn = GetDistinguishedName(ownerName);
            var ownerSid = GetSid(ownerName);
            result.Add(Tuple.Create(eLiamAccessRoles.ADOwner.ToString(), ownerSid, ownerName, ownerDn));


            // 2) Member-Gruppe (Gruppentyp nach Parameter)
            string memberName = null;
            for (int loop = 0; loop <= MaxLoop; loop++)
            {
                string loopPart = loop > 0 ? "_" + loop : string.Empty;
                memberName = memberConv.NamingTemplate
                    .Replace("{{ADGroupPrefix}}", prefix ?? string.Empty)
                    .Replace("{{NAME}}", serviceName)
                    .Replace("{{_LOOP}}", loopPart)
                    .Replace("{{GROUPTYPEPOSTFIX}}", memberPostfix);
                if (!GroupExists(memberName)) break;
                if (loop == MaxLoop) throw new InvalidOperationException($"Kein eindeutiger Member-Name für '{serviceName}' nach {MaxLoop} Versuchen.");
            }
            EnsureGroup(memberName, memberConv, description, managedByDn: ownerDn, gruppenbereich, gruppentyp);
            AddMembersBySid(memberName, memberSidList);    // NEU: SIDs als Member hinzufügen
            var memberDn = GetDistinguishedName(memberName);
            var memberSid = GetSid(memberName);
            result.Add(Tuple.Create(eLiamAccessRoles.ADMember.ToString(), memberSid, memberName, memberDn));

            return result;
        }

        /// <summary>
        /// Fügt einer bestehenden Gruppe per SID die entsprechenden AD-Objekte hinzu.
        /// </summary>
        private void AddMembersBySid(string groupName, IEnumerable<string> sidList)
        {
            if (sidList == null) return;

            // Basis für die Suche: komplette Domäne, nicht nur der OU-Pfad
            string domainRoot = GetLdapDomainRoot();
            using (var root = new DirectoryEntry(domainRoot, _user, _password, AuthenticationTypes.Secure))
            using (var grpSearch = new DirectorySearcher(root))
            {
                grpSearch.Filter = $"(&(objectCategory=group)(sAMAccountName={groupName}))";
                var grpRes = grpSearch.FindOne();
                if (grpRes == null) return;

                var grpEntry = grpRes.GetDirectoryEntry();
                foreach (var sidStr in sidList)
                {
                    // Leere oder null überspringen
                    if (string.IsNullOrWhiteSpace(sidStr))
                        continue;

                    SecurityIdentifier sid;
                    try
                    {
                        sid = new SecurityIdentifier(sidStr);
                    }
                    catch (Exception)
                    {
                        // Ungültige SID-String-Darstellung überspringen
                        continue;
                    }

                    // In LDAP-Filter-Notation umwandeln
                    var bytes = new byte[sid.BinaryLength];
                    sid.GetBinaryForm(bytes, 0);
                    var sb = new StringBuilder();
                    foreach (var b in bytes)
                        sb.AppendFormat("\\{0:X2}", b);
                    string octetSid = sb.ToString();

                    // Suche nach dem Objekt in der Domäne
                    using (var usrSearch = new DirectorySearcher(root))
                    {
                        usrSearch.Filter = $"(objectSid={octetSid})";
                        var usrRes = usrSearch.FindOne();
                        if (usrRes == null)
                            continue;

                        var userDn = usrRes.Properties["distinguishedName"][0].ToString();
                        // Doppelteinträge vermeiden
                        if (!grpEntry.Properties["member"].Contains(userDn))
                            grpEntry.Properties["member"].Add(userDn);
                    }
                }

                grpEntry.CommitChanges();
            }
        }

        /// <summary>
        /// Wandelt eine SID (String-Form) in das für LDAP nötige Oktet-String-Format um.
        /// </summary>
        private string SidStringToLdapFilter(string sidString)
        {
            var sid = new SecurityIdentifier(sidString);
            var bytes = new byte[sid.BinaryLength];
            sid.GetBinaryForm(bytes, 0);
            var sb = new StringBuilder();
            foreach (var b in bytes)
                sb.AppendFormat("\\{0:X2}", b);
            return sb.ToString();
        }


        private string GetSid(string name)
        {
            using (var root = new DirectoryEntry(GetLdapRoot(), _user, _password, AuthenticationTypes.Secure))
            using (var ds = new DirectorySearcher(root))
            {
                ds.Filter = $"(&(objectCategory=group)(sAMAccountName={name}))";
                var r = ds.FindOne();
                if (r == null) return null;
                var de = r.GetDirectoryEntry();
                var sidBytes = (byte[])de.Properties["objectSid"][0];
                return new SecurityIdentifier(sidBytes, 0).Value;
            }
        }


        private string FormatName(cLiamNamingConvention conv, string serviceName, System.Collections.Generic.IDictionary<string, string> tags)
        {
            string tmpl = conv.NamingTemplate.Replace("{{NAME}}", serviceName);
            foreach (var kv in tags)
                tmpl = tmpl.Replace("{{" + kv.Key + "}}", kv.Value);
            return tmpl;
        }

        /// <summary>
        /// Stellt sicher, dass die Gruppe existiert – neu mit Scope & Type.
        /// </summary>
        private void EnsureGroup(
            string groupName,
            cLiamNamingConvention conv,
            string description,
            string managedByDn,
            eLiamAccessRoleScopes groupScope,
            ADGroupType groupType)
        {
            if (!GroupExists(groupName))
            {
                using (var root = new DirectoryEntry(GetLdapRoot(), _user, _password, AuthenticationTypes.Secure))
                {
                    var grp = root.Children.Add("CN=" + groupName, "group");
                    grp.Properties["sAMAccountName"].Value = groupName;
                    grp.Properties["displayName"].Value = groupName;
                    // Hier: Security-Bit (0x80000000) nur, wenn Security, sonst 0
                    int typeBit = (groupType == ADGroupType.Security)
                        ? unchecked((int)0x80000000)
                        : 0;
                    // Scope-Bit aus Param
                    grp.Properties["groupType"].Value = unchecked(typeBit | GetScopeBit(groupScope));

                    if (!string.IsNullOrEmpty(description))
                        grp.Properties["description"].Value = description;
                    if (managedByDn != null)
                        grp.Properties["managedBy"].Value = managedByDn;
                    grp.CommitChanges();
                }
                WaitReplication(groupName, TimeSpan.FromMinutes(2));
            }
        }

        private bool GroupExists(string name)
        {
            return _adBase.directoryEntry.Children.Cast<DirectoryEntry>()
                .Any(c => string.Equals(
                    c.Properties["sAMAccountName"]?.Value?.ToString(), name, StringComparison.OrdinalIgnoreCase));
        }

        private void WaitReplication(string groupName, TimeSpan timeout)
        {
            var sw = System.Diagnostics.Stopwatch.StartNew();
            while (sw.Elapsed < timeout)
            {
                if (GroupExists(groupName))
                    return;
                Thread.Sleep(2000);
            }
        }

        private string GetDistinguishedName(string name)
        {
            using (var root = new DirectoryEntry(GetLdapRoot(), _user, _password, AuthenticationTypes.Secure))
            using (var ds = new DirectorySearcher(root))
            {
                ds.Filter = "(&(objectClass=group)(sAMAccountName=" + name + "))";
                var res = ds.FindOne();
                return res?.Properties["distinguishedName"]?[0]?.ToString();
            }
        }

        private int GetScopeBit(eLiamAccessRoleScopes scope)
        {
            switch (scope)
            {
                case eLiamAccessRoleScopes.Universal:
                    return 0x8;
                case eLiamAccessRoleScopes.Global:
                    return 0x2;
                case eLiamAccessRoleScopes.DomainLocal:
                    return 0x4;
                default:
                    return 0x8;
            }
        }
    }
}
