using dodohold.core;
using Microsoft.AspNetCore.Mvc;
using molilian.core;
using System.Net;
using System.Text.Json;

namespace molilian.api.Controllers
{
    [ApiController]
    [MyAuthorize("admin")]
    [Route("api/[controller]/[action]")]
    public class ProxyController : ControllerBase
    {
        private const string ProxyCheckRequestUrl = "http://cip.cc";
        private const string ProxyCheckUserAgent = "curl/8.7.1";

        readonly IAuthorizationProvider provider = new AdminProvider();
        protected IHttpContextAccessor _accessor;

        public ProxyController(IHttpContextAccessor accessor)
        {
            _accessor = accessor;
        }

        [HttpGet]
        public ActionResult get()
        {
            return new APIResult(new
            {
                data = BuildProxyNodeListResult(1, 20, true, string.Empty, string.Empty)
            });
        }

        [HttpPost]
        public ActionResult list([FromBody] JsonElement form)
        {
            int page = form.Read("current", 1);
            int size = form.Read("pageSize", 20);
            bool getTotal = form.Read("getTotal", true);
            string keyword = form.Read("keyword", string.Empty);
            string status = form.Read("status", string.Empty);

            return new APIResult(new
            {
                data = BuildProxyNodeListResult(page, size, getTotal, keyword, status)
            });
        }

        private static dynamic BuildProxyNodeListResult(
            int page,
            int size,
            bool getTotal,
            string keyword,
            string status
        )
        {
            page = page <= 0 ? 1 : page;
            size = size <= 0 ? 20 : size;

            string filter = string.Empty;
            string likeKeyword = string.Empty;
            IEnumerable<string> matchedNodeNames = Array.Empty<string>();
            bool? statusValue = null;

            if (!string.IsNullOrWhiteSpace(status))
            {
                status = status.Trim().ToLowerInvariant();
                if (status == "enabled")
                {
                    statusValue = true;
                    filter += " AND status=@statusValue";
                }
                else if (status == "disabled")
                {
                    statusValue = false;
                    filter += " AND status=@statusValue";
                }
            }

            if (!string.IsNullOrWhiteSpace(keyword))
            {
                likeKeyword = $"%{keyword.Trim()}%";
                matchedNodeNames = FindMatchedNodeNames(likeKeyword).ToArray();

                filter += " AND (name LIKE @likeKeyword OR nodeName LIKE @likeKeyword OR description LIKE @likeKeyword OR server LIKE @likeKeyword OR end_point LIKE @likeKeyword OR type LIKE @likeKeyword";
                if (matchedNodeNames.Any())
                {
                    filter += " OR name IN @matchedNodeNames OR nodeName IN @matchedNodeNames";
                }
                filter += ")";
            }

            filter = filter.StringTrimStart(" AND ");

            var result = new DBContext.Table("proxy_nodes")
                .Where(filter, new { statusValue, likeKeyword, matchedNodeNames })
                .Page(size, page)
                .Order("status DESC, name ASC")
                .PageList<ProxyNodeListItemDTO>(getTotal);

            List<ProxyNodeAccountSourceDTO> tkAccounts = LoadProxyNodeAccountSources("tk_pool");
            List<ProxyNodeAccountSourceDTO> jdAccounts = LoadProxyNodeAccountSources("jd_pool");
            List<ProxyNodeAccountSourceDTO> pddAccounts = LoadProxyNodeAccountSources("pdd_pool");

            foreach (var node in result.List)
            {
                var accounts = BuildUsageAccounts(node, "tk", tkAccounts)
                    .Concat(BuildUsageAccounts(node, "jd", jdAccounts))
                    .Concat(BuildUsageAccounts(node, "pdd", pddAccounts))
                    .OrderByDescending(item => item.status)
                    .ThenBy(item => item.platform)
                    .ThenBy(item => item.name)
                    .ToList();

                node.accountCount = accounts.Count;
                node.onlineAccountCount = accounts.Count(item => item.status);
                node.offlineAccountCount = accounts.Count(item => !item.status);
                node.accounts = accounts;
            }

            return result;
        }

        [HttpPost]
        public async Task<ActionResult> update([FromBody] JsonElement form)
        {
            var clientIp = _accessor.HttpContext.GetUserIp();
            var token = provider.Get(_accessor.HttpContext);

            int id = form.Read<int>("id", 0);
            string name = form.Read<string>("name", string.Empty);
            string val = form.Read<string>("val", string.Empty);

            if (id == 0)
            {
                return new APIResult(new
                {
                    data = new { success = false, msg = "更新失败，请检查输入参数" }
                });
            }

            int result;
            switch (name)
            {
                case "status":
                    bool bVal = "true".Equals(val, StringComparison.OrdinalIgnoreCase) || "1".Equals(val);
                    result = new DBContext.Table("proxy_nodes")
                        .Add(name, bVal)
                        .Add("last_time", DateTime.Now)
                        .Where("id=@id", new { id })
                        .Update();
                    break;
                default:
                    return new APIResult(new
                    {
                        data = new { success = false, msg = "更新失败，未授权操作" }
                    });
            }

            bool success = result > 0;
            if (success)
            {
                OperationLogCore.LogOperation(token.AccessKey, clientIp, $"proxy_nodes:{id}:{name}", string.Empty, val);
                ProxyNodesCore.Refresh();
                await EndPointCore.NotifyReload(true);
            }

            return new APIResult(new
            {
                data = new { success, msg = success ? "更新成功" : "更新失败，请检查输入信息" }
            });
        }

        [HttpGet]
        public async Task<ActionResult> check(int id, string target = "internal")
        {
            var proxyNode = new DBContext.Table("proxy_nodes").Get<ProxyNodesDTO>("id=@id", new { id });
            if (proxyNode == null)
            {
                return new APIResult(new
                {
                    data = new { success = false, msg = "没有匹配的 proxy_nodes 记录" }
                });
            }

            target = string.IsNullOrWhiteSpace(target) ? "internal" : target.Trim().ToLowerInvariant();
            if (target is not ("internal" or "external"))
            {
                return new APIResult(new
                {
                    data = new { success = false, msg = "target 仅支持 internal 或 external" }
                });
            }

            string externalProxyAddress = BuildExternalProxyAddress(proxyNode.id);

            var data = new ProxyNodeCheckResponseDTO
            {
                success = false,
                msg = "未执行检测",
            };

            if (target == "internal")
            {
                var internalResult = await CheckProxyAsync(
                    proxyNode.server,
                    proxyNode.username,
                    proxyNode.password
                );
                data.success = internalResult.success;
                data.msg = internalResult.success ? "内网代理检测成功" : "内网代理检测失败";
                data.check = new ProxyCheckChannelDTO
                {
                    address = proxyNode.server,
                    success = internalResult.success,
                    result = internalResult.result,
                };
            }
            else
            {
                var externalResult = await CheckProxyAsync(
                    BuildProxyUri(externalProxyAddress, proxyNode.type),
                    proxyNode.username,
                    proxyNode.password
                );
                data.success = externalResult.success;
                data.msg = externalResult.success ? "外网代理检测成功" : "外网代理检测失败";
                data.check = new ProxyCheckChannelDTO
                {
                    address = externalProxyAddress,
                    success = externalResult.success,
                    result = externalResult.result,
                };
            }

            return new APIResult(new
            {
                data
            });
        }

        [HttpGet]
        public async Task<ActionResult> ChangePublicIpByName(string nodeName)
        {
            var proxyNode = new DBContext.Table("proxy_nodes")
                .Get<dynamic>("(nodeName=@nodeName OR name=@nodeName)", new { nodeName });
            if (proxyNode == null)
            {
                return new APIResult(new
                {
                    data = new { success = false, msg = "没有匹配的 proxy_nodes 记录" }
                });
            }

            int aliyunId = proxyNode.aliyun_id;
            string proxyServer = proxyNode.server;

            var account = new DBContext.Table("aliyun_pool").Get<AliyunPoolDTO>(aliyunId);
            if (account == null)
            {
                return new APIResult(new
                {
                    data = new { success = false, msg = "没有匹配的 aliyun_pool 记录" }
                });
            }

            var uri = new Uri(proxyServer);
            string privateIp = uri.Host;
            AliyunCore core = new(account);
            var (success, oldIp, newIp) = await core.ChangePublicIpAsync(privateIp);

            return new APIResult(new
            {
                data = new
                {
                    success,
                    msg = success ? $"更换公网 IP 成功：{oldIp} -> {newIp}" : "更换公网 IP 失败"
                }
            });
        }

        [HttpGet]
        public async Task<ActionResult> ChangePublicIp(int id)
        {
            id = id >= 20000 ? id - 20000 : id;

            var proxyNode = new DBContext.Table("proxy_nodes").Get<dynamic>(id);
            if (proxyNode == null)
            {
                return new APIResult(new
                {
                    data = new { success = false, msg = "没有匹配的 proxy_nodes 记录" }
                });
            }

            int aliyunId = proxyNode.aliyun_id;
            string proxyServer = proxyNode.server;

            var account = new DBContext.Table("aliyun_pool").Get<AliyunPoolDTO>(aliyunId);
            if (account == null)
            {
                return new APIResult(new
                {
                    data = new { success = false, msg = "没有匹配的 aliyun_pool 记录" }
                });
            }

            var uri = new Uri(proxyServer);
            string privateIp = uri.Host;
            AliyunCore core = new(account);
            var (success, oldIp, newIp) = await core.ChangePublicIpAsync(privateIp);

            return new APIResult(new
            {
                data = new
                {
                    success,
                    msg = success ? $"更换公网 IP 成功：{oldIp} -> {newIp}" : "更换公网 IP 失败"
                }
            });
        }

        private static IEnumerable<ProxyNodeUsageAccountDTO> BuildUsageAccounts(
            ProxyNodeListItemDTO node,
            string platform,
            IEnumerable<ProxyNodeAccountSourceDTO> accounts
        )
        {
            var nodeNames = GetProxyNodeMatchNames(node).ToArray();
            if (!nodeNames.Any())
            {
                return Enumerable.Empty<ProxyNodeUsageAccountDTO>();
            }

            return accounts
                .Where(account => MatchNodeName(account.nodeName, nodeNames))
                .Select(account => new ProxyNodeUsageAccountDTO
                {
                    id = account.id,
                    platform = platform,
                    name = account.name,
                    company = account.company,
                    nodeName = account.nodeName,
                    status = account.status,
                });
        }

        private static bool MatchNodeName(
            string accountNodes,
            IEnumerable<string> nodeNames
        )
        {
            var nodeNameSet = nodeNames
                .Where(item => !string.IsNullOrWhiteSpace(item))
                .Select(item => item.Trim())
                .ToHashSet(StringComparer.OrdinalIgnoreCase);

            if (nodeNameSet.Count == 0)
            {
                return false;
            }

            return SplitNodeNames(accountNodes)
                .Any(item => nodeNameSet.Contains(item));
        }

        private static IEnumerable<string> GetProxyNodeMatchNames(ProxyNodeListItemDTO node)
        {
            return new[] { node.nodeName, node.name }
                .Where(item => !string.IsNullOrWhiteSpace(item))
                .Select(item => item.Trim())
                .Distinct(StringComparer.OrdinalIgnoreCase);
        }

        private static IEnumerable<string> FindMatchedNodeNames(string keyword)
        {
            if (string.IsNullOrWhiteSpace(keyword))
            {
                return Array.Empty<string>();
            }

            const string accountFilter = "name LIKE @keyword OR company LIKE @keyword OR nodename LIKE @keyword";

            List<ProxyNodeNameSourceDTO> allNodes = new();
            allNodes.AddRange(LoadProxyNodeNameSources("tk_pool", accountFilter, keyword));
            allNodes.AddRange(LoadProxyNodeNameSources("jd_pool", accountFilter, keyword));
            allNodes.AddRange(LoadProxyNodeNameSources("pdd_pool", accountFilter, keyword));

            return allNodes
                .SelectMany(item => SplitNodeNames(item.nodeName))
                .Distinct(StringComparer.OrdinalIgnoreCase)
                .ToList();
        }

        private static List<ProxyNodeAccountSourceDTO> LoadProxyNodeAccountSources(string tableName)
        {
            return new DBContext.Table(tableName)
                .Fields("id, name, company, status, nodename AS nodeName")
                .Select<ProxyNodeAccountSourceDTO>()?
                .ToList() ?? new List<ProxyNodeAccountSourceDTO>();
        }

        private static List<ProxyNodeNameSourceDTO> LoadProxyNodeNameSources(
            string tableName,
            string filter,
            string keyword
        )
        {
            return new DBContext.Table(tableName)
                .Fields("nodename AS nodeName")
                .Where(filter, new { keyword })
                .Select<ProxyNodeNameSourceDTO>()?
                .ToList() ?? new List<ProxyNodeNameSourceDTO>();
        }

        private static IEnumerable<string> SplitNodeNames(string accountNodes)
        {
            if (string.IsNullOrWhiteSpace(accountNodes))
            {
                return Array.Empty<string>();
            }

            return accountNodes
                .Replace('，', ',')
                .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
        }

        private static string BuildExternalProxyAddress(int id)
        {
            return $"bjapi.molilian.com:20{id:D3}";
        }

        private static string BuildProxyUri(string address, string type)
        {
            if (string.IsNullOrWhiteSpace(address))
            {
                return string.Empty;
            }

            address = address.Trim();
            if (address.Contains("://", StringComparison.Ordinal) &&
                Uri.TryCreate(address, UriKind.Absolute, out var absoluteUri))
            {
                return absoluteUri.AbsoluteUri;
            }

            string scheme = string.IsNullOrWhiteSpace(type) ? "http" : type.Trim().ToLowerInvariant();
            if (scheme is not ("http" or "https" or "socks4" or "socks4a" or "socks5"))
            {
                scheme = "http";
            }
            return $"{scheme}://{address}";
        }

        private static async Task<(bool success, string result)> CheckProxyAsync(
            string proxyAddress,
            string username,
            string password
        )
        {
            try
            {
                string proxyUri = BuildProxyUri(proxyAddress, "http");
                WebClientUtility client = new()
                {
                    Timeout = TimeSpan.FromSeconds(3),
                    UserAgent = ProxyCheckUserAgent,
                };

                if (string.IsNullOrWhiteSpace(proxyUri))
                {
                    return (false, "代理地址为空");
                }

                client.Proxy = new WebProxy
                {
                    Address = new Uri(proxyUri),
                    Credentials = string.IsNullOrWhiteSpace(username) && string.IsNullOrWhiteSpace(password)
                        ? null
                        : new NetworkCredential(username, password)
                };

                var response = await client.RequestAsync(ProxyCheckRequestUrl);
                string body = response.Body()?.Trim() ?? string.Empty;

                if (response.Successed)
                {
                    return (true, string.IsNullOrWhiteSpace(body) ? "代理可用，但目标站点无返回内容" : body);
                }

                if (response.ResponseException != null)
                {
                    return (false, response.ResponseException.GetBaseException().Message);
                }

                if (response.ResponseMessage != null)
                {
                    string message = $"HTTP {(int)response.ResponseMessage.StatusCode}";
                    if (!string.IsNullOrWhiteSpace(body))
                    {
                        message += $" - {body}";
                    }
                    return (false, message);
                }

                if (!string.IsNullOrWhiteSpace(body))
                {
                    return (false, body);
                }

                return (false, "请求失败，但未返回异常或响应内容");
            }
            catch (TaskCanceledException)
            {
                return (false, "请求超时（3s）");
            }
            catch (Exception ex)
            {
                return (false, ex.GetBaseException().Message);
            }
        }
    }

    public class ProxyNodeAccountSourceDTO
    {
        public int id { get; set; }
        public string name { get; set; } = string.Empty;
        public string company { get; set; } = string.Empty;
        public bool status { get; set; }
        public string nodeName { get; set; } = string.Empty;
    }

    public class ProxyNodeNameSourceDTO
    {
        public string nodeName { get; set; } = string.Empty;
    }

    public class ProxyNodeUsageAccountDTO
    {
        public int id { get; set; }
        public string platform { get; set; } = string.Empty;
        public string name { get; set; } = string.Empty;
        public string company { get; set; } = string.Empty;
        public string nodeName { get; set; } = string.Empty;
        public bool status { get; set; }
    }

    public class ProxyNodeListItemDTO
    {
        public int id { get; set; }
        public bool status { get; set; }
        public string name { get; set; } = string.Empty;
        public string nodeName { get; set; } = string.Empty;
        public string description { get; set; } = string.Empty;
        public string end_point { get; set; } = string.Empty;
        public string type { get; set; } = string.Empty;
        public string server { get; set; } = string.Empty;
        public string username { get; set; } = string.Empty;
        public DateTime create_time { get; set; }
        public DateTime last_time { get; set; }
        public int accountCount { get; set; }
        public int onlineAccountCount { get; set; }
        public int offlineAccountCount { get; set; }
        public List<ProxyNodeUsageAccountDTO> accounts { get; set; } = new();
    }

    public class ProxyCheckChannelDTO
    {
        public string address { get; set; } = string.Empty;
        public bool success { get; set; }
        public string result { get; set; } = string.Empty;
    }

    public class ProxyNodeCheckResponseDTO
    {
        public bool success { get; set; }
        public string msg { get; set; } = string.Empty;
        public ProxyCheckChannelDTO? check { get; set; }
    }
}
