using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEngine;

namespace Project.Tasks.Editor
{
    internal static class TaskBoardService
    {
        private static readonly string[] EditableSectionOrder =
        {
            "Why",
            "Expected Outcome",
            "Current Context",
            "Acceptance Criteria",
            "Verification",
            "Risks / Open Questions",
            "Human Decisions Needed",
            "Decision Log",
            "Handoff Notes",
        };

        private static readonly Regex HeadingRegex = new Regex(@"^##\s+(.+)$", RegexOptions.Compiled);
        private static readonly Regex H1Regex = new Regex(@"^#\s+(.+)$", RegexOptions.Compiled);
        private static readonly Regex JiraTimeRegex = new Regex(@"^(?:(\d+)w)?(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)m)?$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
        private static readonly Regex ExecutionTokenRegex = new Regex(@"(?i)(\d+)\s*([wdhm])", RegexOptions.Compiled);

        public static string NormalizePriority(string value)
        {
            if (string.IsNullOrWhiteSpace(value))
            {
                return string.Empty;
            }

            switch (value.Trim())
            {
                case "lowest":
                case "Lowest":
                    return "Lowest";
                case "low":
                case "Low":
                    return "Low";
                case "medium":
                case "Medium":
                    return "Medium";
                case "high":
                case "High":
                    return "High";
                case "highest":
                case "Highest":
                    return "Highest";
                default:
                    return value.Trim();
            }
        }

        public static string NormalizeStatus(string value)
        {
            if (string.IsNullOrWhiteSpace(value))
            {
                return string.Empty;
            }

            switch (value.Trim())
            {
                case "proposal":
                case "backlog":
                case "BackLog":
                    return "BackLog";
                case "ready":
                case "todo":
                case "ToDo":
                    return "ToDo";
                case "in_progress":
                case "inprogress":
                case "InProgress":
                    return "InProgress";
                case "blocked":
                case "review":
                case "Review":
                    return "Review";
                case "done":
                case "Done":
                    return "Done";
                default:
                    return value.Trim();
            }
        }

        public static int GetPrioritySortOrder(string value)
        {
            switch (NormalizePriority(value))
            {
                case "Highest":
                    return 0;
                case "High":
                    return 1;
                case "Medium":
                    return 2;
                case "Low":
                    return 3;
                case "Lowest":
                    return 4;
                default:
                    return int.MaxValue;
            }
        }

        public static TaskBoardData Load()
        {
            var data = new TaskBoardData();
            data.ProjectRoot = GetProjectRoot();
            data.TasksDirectory = NormalizePath(Path.Combine(data.ProjectRoot, "docs", "tasks"));
            data.IndexPath = NormalizePath(Path.Combine(data.TasksDirectory, "Index.md"));
            data.OwnersConfigPath = "Assets/Features/Tasks/Editor/TaskBoardOwners.asset";
            LoadOwnerPresets(data);

            if (!Directory.Exists(data.TasksDirectory))
            {
                data.Warnings.Add("Каталог docs/tasks не найден.");
                return data;
            }

            if (!File.Exists(data.IndexPath))
            {
                data.Warnings.Add("Файл docs/tasks/Index.md не найден.");
                return data;
            }

            string[] lines = File.ReadAllLines(data.IndexPath, Encoding.UTF8);
            ParseIndex(data, lines);

            data.Tasks.Sort(CompareTaskIds);
            return data;
        }

        public static void LoadTaskDetails(TaskRecord task)
        {
            if (task == null || task.DetailsLoaded)
            {
                return;
            }

            task.DetailsLoaded = true;

            if (!task.FileExists || string.IsNullOrEmpty(task.AbsoluteFilePath) || !File.Exists(task.AbsoluteFilePath))
            {
                if (string.IsNullOrEmpty(task.Title))
                {
                    task.Title = DeriveTitleFromPath(task.RelativeFilePath, task.Id);
                }
                return;
            }

            PopulateTaskFromFile(task);
            ValidateTask(task);
        }

        public static bool Save(TaskBoardData data, TaskRecord updatedTask, out string error)
        {
            error = null;

            try
            {
                CanonicalizeTask(updatedTask);
                SaveIndexRow(data.IndexPath, updatedTask);

                if (updatedTask.FileExists && File.Exists(updatedTask.AbsoluteFilePath))
                {
                    SaveTaskFile(updatedTask.AbsoluteFilePath, updatedTask);
                }

                return true;
            }
            catch (Exception exception)
            {
                error = exception.Message;
                return false;
            }
        }

        public static bool DeleteTask(TaskBoardData data, TaskRecord task, out string error)
        {
            error = null;

            try
            {
                if (data == null)
                {
                    throw new InvalidOperationException("Данные task board не загружены.");
                }

                if (task == null)
                {
                    throw new InvalidOperationException("Задача не выбрана.");
                }

                DeleteIndexRow(data.IndexPath, task);

                if (task.FileExists && !string.IsNullOrWhiteSpace(task.AbsoluteFilePath) && File.Exists(task.AbsoluteFilePath))
                {
                    File.Delete(task.AbsoluteFilePath);
                }

                return true;
            }
            catch (Exception exception)
            {
                error = exception.Message;
                return false;
            }
        }

        public static bool TryParseExecutionTimeToMinutes(string value, out int minutes)
        {
            minutes = 0;
            string normalized;
            if (!TryNormalizeExecutionTime(value, out normalized, out minutes))
            {
                return false;
            }

            return true;
        }

        public static string NormalizeExecutionTime(string value)
        {
            string normalized;
            int minutes;
            return TryNormalizeExecutionTime(value, out normalized, out minutes) ? normalized : (string.IsNullOrWhiteSpace(value) ? string.Empty : value.Trim());
        }

        public static bool TryNormalizeExecutionTime(string value, out string normalized, out int totalMinutes)
        {
            normalized = string.Empty;
            totalMinutes = 0;

            if (string.IsNullOrWhiteSpace(value))
            {
                return false;
            }

            string input = value.Trim();
            MatchCollection matches = ExecutionTokenRegex.Matches(input);
            if (matches.Count == 0)
            {
                return false;
            }

            string leftover = ExecutionTokenRegex.Replace(input, string.Empty);
            leftover = Regex.Replace(leftover, @"\s+", string.Empty);
            if (!string.IsNullOrEmpty(leftover))
            {
                return false;
            }

            int weeks = 0;
            int days = 0;
            int hours = 0;
            int minutes = 0;

            foreach (Match match in matches)
            {
                int amount = ParseNumber(match.Groups[1].Value);
                string unit = match.Groups[2].Value.ToLowerInvariant();
                switch (unit)
                {
                    case "w":
                        weeks += amount;
                        break;
                    case "d":
                        days += amount;
                        break;
                    case "h":
                        hours += amount;
                        break;
                    case "m":
                        minutes += amount;
                        break;
                    default:
                        return false;
                }
            }

            totalMinutes = (weeks * TaskBoardConstants.MinutesPerWeek)
                + (days * TaskBoardConstants.MinutesPerDay)
                + (hours * TaskBoardConstants.MinutesPerHour)
                + minutes;

            if (totalMinutes <= 0 || totalMinutes % 30 != 0)
            {
                return false;
            }

            normalized = BuildCanonicalExecutionTime(totalMinutes);
            return true;
        }

        public static string FormatMinutesForDisplay(int totalMinutes)
        {
            if (totalMinutes < 0)
            {
                return "-";
            }

            int remaining = totalMinutes;
            int days = remaining / TaskBoardConstants.MinutesPerDay;
            remaining %= TaskBoardConstants.MinutesPerDay;
            int hours = remaining / TaskBoardConstants.MinutesPerHour;
            remaining %= TaskBoardConstants.MinutesPerHour;
            int minutes = remaining;

            var parts = new List<string>();
            if (days > 0)
            {
                parts.Add(days + "d");
            }

            if (hours > 0)
            {
                parts.Add(hours + "h");
            }

            if (minutes > 0 || parts.Count == 0)
            {
                parts.Add(minutes + "m");
            }

            return string.Join(" ", parts) + " (" + totalMinutes + "m)";
        }

        public static string FormatExecutionTimeForDisplay(string value)
        {
            string normalized;
            int totalMinutes;
            if (!TryNormalizeExecutionTime(value, out normalized, out totalMinutes))
            {
                return string.IsNullOrWhiteSpace(value) ? "-" : value;
            }

            Match match = JiraTimeRegex.Match(normalized);
            var parts = new List<string>();

            if (match.Groups[1].Success)
            {
                parts.Add(match.Groups[1].Value + "w");
            }

            if (match.Groups[2].Success)
            {
                parts.Add(match.Groups[2].Value + "d");
            }

            if (match.Groups[3].Success)
            {
                parts.Add(match.Groups[3].Value + "h");
            }

            if (match.Groups[4].Success)
            {
                parts.Add(match.Groups[4].Value + "m");
            }

            if (parts.Count == 0)
            {
                parts.Add("0m");
            }

            return string.Join(" ", parts) + " (" + totalMinutes + "m)";
        }

        public static int SumEstimatedMinutes(IEnumerable<TaskRecord> tasks)
        {
            int total = 0;
            foreach (TaskRecord task in tasks)
            {
                if (task != null && task.EstimatedMinutes >= 0)
                {
                    total += task.EstimatedMinutes;
                }
            }

            return total;
        }

        public static string[] BuildOwnerOptions(TaskBoardData data, IEnumerable<TaskRecord> tasks)
        {
            var values = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "unassigned" };

            if (data != null)
            {
                foreach (string owner in data.OwnerPresets)
                {
                    if (!string.IsNullOrWhiteSpace(owner))
                    {
                        values.Add(owner.Trim());
                    }
                }
            }

            if (tasks != null)
            {
                foreach (TaskRecord task in tasks)
                {
                    if (task != null && !string.IsNullOrWhiteSpace(task.Owner))
                    {
                        values.Add(task.Owner.Trim());
                    }
                }
            }

            return values.OrderBy(value => value, StringComparer.OrdinalIgnoreCase).ToArray();
        }

        public static bool CanonicalizeTask(TaskRecord task)
        {
            if (task == null)
            {
                return false;
            }

            bool changed = false;

            string normalizedStatus = NormalizeStatus(task.Status);
            if (!string.Equals(task.Status, normalizedStatus, StringComparison.Ordinal))
            {
                task.Status = normalizedStatus;
                changed = true;
            }

            string normalizedPriority = NormalizePriority(task.Priority);
            if (!string.Equals(task.Priority, normalizedPriority, StringComparison.Ordinal))
            {
                task.Priority = normalizedPriority;
                changed = true;
            }

            string normalizedOwner = string.IsNullOrWhiteSpace(task.Owner) ? "unassigned" : task.Owner.Trim();
            if (!string.Equals(task.Owner, normalizedOwner, StringComparison.Ordinal))
            {
                task.Owner = normalizedOwner;
                changed = true;
            }

            if (string.IsNullOrWhiteSpace(task.Area))
            {
                task.Area = "-";
                changed = true;
            }

            string normalizedExecutionTime;
            int totalMinutes;
            if (TryNormalizeExecutionTime(task.ExecutionTime, out normalizedExecutionTime, out totalMinutes))
            {
                if (!string.Equals(task.ExecutionTime, normalizedExecutionTime, StringComparison.Ordinal))
                {
                    task.ExecutionTime = normalizedExecutionTime;
                    changed = true;
                }

                task.EstimatedMinutes = totalMinutes;
            }
            else
            {
                task.EstimatedMinutes = -1;
            }

            return changed;
        }

        public static bool CreateTaskFileFromTemplate(TaskBoardData data, TaskRecord task, out string error)
        {
            error = null;

            try
            {
                if (data == null)
                {
                    throw new InvalidOperationException("Данные task board не загружены.");
                }

                if (task == null)
                {
                    throw new InvalidOperationException("Задача не выбрана.");
                }

                if (string.IsNullOrWhiteSpace(task.RelativeFilePath))
                {
                    throw new InvalidOperationException("В Index.md не указан путь к task-файлу.");
                }

                string relativePath = NormalizePath(task.RelativeFilePath);
                if (!relativePath.StartsWith("docs/tasks/", StringComparison.OrdinalIgnoreCase))
                {
                    throw new InvalidOperationException("Task-файл можно создавать только внутри docs/tasks.");
                }

                string absolutePath = NormalizePath(Path.Combine(data.ProjectRoot, relativePath));
                if (File.Exists(absolutePath))
                {
                    throw new InvalidOperationException("Task-файл уже существует.");
                }

                string templatePath = NormalizePath(Path.Combine(data.TasksDirectory, "_template.md"));
                if (!File.Exists(templatePath))
                {
                    throw new InvalidOperationException("Шаблон docs/tasks/_template.md не найден.");
                }

                string template = File.ReadAllText(templatePath, Encoding.UTF8);
                string today = DateTime.Now.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
                string title = string.IsNullOrWhiteSpace(task.Title) ? task.Id : task.Title.Trim();
                string owner = string.IsNullOrWhiteSpace(task.Owner) ? "unassigned" : task.Owner.Trim();
                string created = string.IsNullOrWhiteSpace(task.Created) ? today : task.Created.Trim();
                string area = string.IsNullOrWhiteSpace(task.Area) ? "docs" : task.Area.Trim();
                string priority = NormalizePriority(task.Priority);
                if (string.IsNullOrWhiteSpace(priority))
                {
                    priority = "Medium";
                }

                string executionTime = string.IsNullOrWhiteSpace(task.ExecutionTime) ? "1d" : task.ExecutionTime.Trim();

                string content = template
                    .Replace("id: TASK-XXXX", "id: " + task.Id)
                    .Replace("title: Короткий заголовок", "title: " + title)
                    .Replace("summary: Короткое описание задачи", "summary: " + EscapeFrontMatterValue(string.IsNullOrWhiteSpace(task.IndexSummary) ? title : task.IndexSummary))
                    .Replace("priority: Medium", "priority: " + priority)
                    .Replace("area: docs", "area: " + area)
                    .Replace("owner: unassigned", "owner: " + owner)
                    .Replace("created: YYYY-MM-DD", "created: " + created)
                    .Replace("updated: YYYY-MM-DD", "updated: " + today)
                    .Replace("execution_time: 1d6h30m", "execution_time: " + executionTime)
                    .Replace("# TASK-XXXX - Короткий заголовок", "# " + task.Id + " - " + title);

                string directory = Path.GetDirectoryName(absolutePath);
                if (!string.IsNullOrEmpty(directory))
                {
                    Directory.CreateDirectory(directory);
                }

                File.WriteAllText(absolutePath, content, new UTF8Encoding(false));
                task.AbsoluteFilePath = absolutePath;
                task.FileExists = true;
                task.DetailsLoaded = false;
                return true;
            }
            catch (Exception exception)
            {
                error = exception.Message;
                return false;
            }
        }

        public static void OpenInDefaultApp(string absolutePath)
        {
            if (string.IsNullOrEmpty(absolutePath) || !File.Exists(absolutePath))
            {
                return;
            }

            var info = new ProcessStartInfo
            {
                FileName = absolutePath,
                UseShellExecute = true,
            };

            Process.Start(info);
        }

        public static bool IsValidExecutionTime(string value)
        {
            string normalized;
            int totalMinutes;
            return TryNormalizeExecutionTime(value, out normalized, out totalMinutes);
        }

        public static string GetProjectRoot()
        {
            DirectoryInfo parent = Directory.GetParent(Application.dataPath);
            return parent != null ? NormalizePath(parent.FullName) : NormalizePath(Application.dataPath);
        }

        public static string NormalizePath(string path)
        {
            return string.IsNullOrEmpty(path) ? string.Empty : path.Replace('\\', '/');
        }

        public static string GetRelativePath(string rootPath, string fullPath)
        {
            Uri rootUri = new Uri(AppendDirectorySeparator(rootPath));
            Uri fullUri = new Uri(fullPath);
            return Uri.UnescapeDataString(rootUri.MakeRelativeUri(fullUri).ToString()).Replace('\\', '/');
        }

        private static string AppendDirectorySeparator(string path)
        {
            if (string.IsNullOrEmpty(path))
            {
                return string.Empty;
            }

            if (path[path.Length - 1] == Path.DirectorySeparatorChar || path[path.Length - 1] == Path.AltDirectorySeparatorChar)
            {
                return path;
            }

            return path + Path.DirectorySeparatorChar;
        }

        private static void ParseIndex(TaskBoardData data, string[] lines)
        {
            int registryIndex = Array.FindIndex(lines, line => string.Equals(line.Trim(), "## Task Registry", StringComparison.Ordinal));
            if (registryIndex < 0)
            {
                data.Warnings.Add("В Index.md не найдена секция '## Task Registry'.");
                return;
            }

            int headerIndex = -1;
            int separatorIndex = -1;
            for (int i = registryIndex + 1; i < lines.Length; i++)
            {
                string trimmed = lines[i].Trim();
                if (trimmed.StartsWith("|", StringComparison.Ordinal))
                {
                    if (headerIndex < 0)
                    {
                        headerIndex = i;
                    }
                    else
                    {
                        separatorIndex = i;
                        break;
                    }
                }
                else if (!string.IsNullOrWhiteSpace(trimmed))
                {
                    break;
                }
            }

            if (headerIndex < 0 || separatorIndex < 0)
            {
                data.Warnings.Add("В Index.md не удалось прочитать таблицу реестра задач.");
                return;
            }

            for (int i = separatorIndex + 1; i < lines.Length; i++)
            {
                string line = lines[i];
                string trimmed = line.Trim();
                if (string.IsNullOrWhiteSpace(trimmed))
                {
                    break;
                }

                if (!trimmed.StartsWith("|", StringComparison.Ordinal))
                {
                    break;
                }

                string[] columns = SplitMarkdownRow(trimmed);
                if (columns.Length < 7)
                {
                    continue;
                }

                bool hasOwnerColumn = columns.Length >= 8;

                var task = new TaskRecord
                {
                    Id = columns[0],
                    Status = NormalizeStatus(columns[1]),
                    Priority = NormalizePriority(columns[2]),
                    Area = columns[3],
                    Owner = hasOwnerColumn ? columns[4] : "unassigned",
                    ExecutionTime = NormalizeExecutionTime(hasOwnerColumn ? columns[5] : columns[4]),
                    RelativeFilePath = StripTicks(hasOwnerColumn ? columns[6] : columns[5]),
                    IndexSummary = hasOwnerColumn ? columns[7] : columns[6],
                    IndexLineNumber = i,
                };

                if (!string.IsNullOrEmpty(task.RelativeFilePath))
                {
                    task.AbsoluteFilePath = NormalizePath(Path.Combine(data.ProjectRoot, task.RelativeFilePath));
                    task.FileExists = File.Exists(task.AbsoluteFilePath);
                    task.Title = DeriveTitleFromPath(task.RelativeFilePath, task.Id);
                }

                if (!task.FileExists)
                {
                    task.ValidationMessages.Add("Task-файл не найден по пути из реестра.");
                }

                int estimatedMinutes;
                task.EstimatedMinutes = TryParseExecutionTimeToMinutes(task.ExecutionTime, out estimatedMinutes) ? estimatedMinutes : -1;
                ValidateTask(task);
                data.Tasks.Add(task);
            }
        }

        private static void PopulateTaskFromFile(TaskRecord task)
        {
            task.TaskSummary = string.Empty;
            task.Header = string.Empty;
            task.Why = string.Empty;
            task.ExpectedOutcome = string.Empty;
            task.CurrentContext = string.Empty;
            task.AcceptanceCriteria = string.Empty;
            task.Verification = string.Empty;
            task.Risks = string.Empty;
            task.HumanDecisions = string.Empty;
            task.DecisionLog = string.Empty;
            task.HandoffNotes = string.Empty;

            if (!task.FileExists || string.IsNullOrEmpty(task.AbsoluteFilePath))
            {
                if (string.IsNullOrEmpty(task.Title))
                {
                    task.Title = DeriveTitleFromPath(task.RelativeFilePath, task.Id);
                }
                return;
            }

            string[] lines = File.ReadAllLines(task.AbsoluteFilePath, Encoding.UTF8);
            int bodyStart = 0;

            if (lines.Length > 0 && string.Equals(lines[0].Trim(), "---", StringComparison.Ordinal))
            {
                for (int i = 1; i < lines.Length; i++)
                {
                    if (string.Equals(lines[i].Trim(), "---", StringComparison.Ordinal))
                    {
                        bodyStart = i + 1;
                        break;
                    }

                    string line = lines[i];
                    int separator = line.IndexOf(':');
                    if (separator <= 0)
                    {
                        continue;
                    }

                    string key = line.Substring(0, separator).Trim();
                    string value = line.Substring(separator + 1).Trim();
                    ApplyFrontMatterField(task, key, value);
                }
            }

            var sections = new Dictionary<string, StringBuilder>(StringComparer.OrdinalIgnoreCase);
            string currentSection = null;

            for (int i = bodyStart; i < lines.Length; i++)
            {
                string line = lines[i];
                Match h1Match = H1Regex.Match(line);
                if (h1Match.Success && string.IsNullOrEmpty(task.Header))
                {
                    task.Header = h1Match.Groups[1].Value.Trim();
                    if (string.IsNullOrEmpty(task.Title))
                    {
                        task.Title = ExtractTitleFromHeader(task.Header, task.Id);
                    }
                    continue;
                }

                Match headingMatch = HeadingRegex.Match(line);
                if (headingMatch.Success)
                {
                    currentSection = headingMatch.Groups[1].Value.Trim();
                    if (!sections.ContainsKey(currentSection))
                    {
                        sections[currentSection] = new StringBuilder();
                    }
                    continue;
                }

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

                if (sections[currentSection].Length > 0)
                {
                    sections[currentSection].AppendLine();
                }

                sections[currentSection].Append(line);
            }

            task.Why = GetSection(sections, "Why");
            task.ExpectedOutcome = GetSection(sections, "Expected Outcome");
            task.CurrentContext = GetSection(sections, "Current Context");
            task.AcceptanceCriteria = GetSection(sections, "Acceptance Criteria");
            task.Verification = GetSection(sections, "Verification");
            task.Risks = GetSection(sections, "Risks / Open Questions");
            task.HumanDecisions = GetSection(sections, "Human Decisions Needed");
            task.DecisionLog = GetSection(sections, "Decision Log");
            task.HandoffNotes = GetSection(sections, "Handoff Notes");

            if (string.IsNullOrEmpty(task.Title))
            {
                task.Title = task.Id;
            }
        }

        private static void ApplyFrontMatterField(TaskRecord task, string key, string value)
        {
            switch (key)
            {
                case "id":
                    if (!string.Equals(task.Id, value, StringComparison.OrdinalIgnoreCase))
                    {
                        task.ValidationMessages.Add("ID в task-файле не совпадает с записью в Index.md.");
                    }
                    break;
                case "title":
                    task.Title = value;
                    break;
                case "summary":
                    task.TaskSummary = value;
                    break;
                case "priority":
                    string normalizedPriority = NormalizePriority(value);
                    if (!string.IsNullOrEmpty(normalizedPriority) && !string.Equals(task.Priority, normalizedPriority, StringComparison.OrdinalIgnoreCase))
                    {
                        task.ValidationMessages.Add("Priority в task-файле не совпадает с реестром.");
                    }
                    break;
                case "area":
                    if (!string.IsNullOrEmpty(value) && !string.Equals(task.Area, value, StringComparison.OrdinalIgnoreCase))
                    {
                        task.ValidationMessages.Add("Area в task-файле не совпадает с реестром.");
                    }
                    break;
                case "owner":
                    if (!string.IsNullOrWhiteSpace(value) && !string.Equals(task.Owner, value, StringComparison.OrdinalIgnoreCase))
                    {
                        task.ValidationMessages.Add("Owner в task-файле не совпадает с реестром.");
                    }
                    break;
                case "created":
                    task.Created = value;
                    break;
                case "updated":
                    task.Updated = value;
                    break;
                case "execution_time":
                    string normalizedExecutionTime = NormalizeExecutionTime(value);
                    if (!string.IsNullOrEmpty(normalizedExecutionTime) && !string.Equals(task.ExecutionTime, normalizedExecutionTime, StringComparison.OrdinalIgnoreCase))
                    {
                        task.ValidationMessages.Add("execution_time в task-файле не совпадает с реестром.");
                    }
                    break;
            }
        }

        private static void ValidateTask(TaskRecord task)
        {
            task.Status = NormalizeStatus(task.Status);
            if (Array.IndexOf(TaskBoardConstants.Statuses, task.Status) < 0)
            {
                task.ValidationMessages.Add("Неизвестный статус в Index.md: " + task.Status);
            }

            task.Priority = NormalizePriority(task.Priority);
            if (Array.IndexOf(TaskBoardConstants.Priorities, task.Priority) < 0)
            {
                task.ValidationMessages.Add("Неизвестный приоритет в Index.md: " + task.Priority);
            }

            task.ExecutionTime = NormalizeExecutionTime(task.ExecutionTime);
            if (!IsValidExecutionTime(task.ExecutionTime))
            {
                task.ValidationMessages.Add("execution_time не соответствует Jira-формату или не кратен 30 минутам.");
            }
            else
            {
                int estimatedMinutes;
                task.EstimatedMinutes = TryParseExecutionTimeToMinutes(task.ExecutionTime, out estimatedMinutes) ? estimatedMinutes : -1;
            }

            if (string.IsNullOrEmpty(task.RelativeFilePath))
            {
                task.ValidationMessages.Add("В реестре не указан путь к task-файлу.");
            }
        }

        private static void SaveIndexRow(string indexPath, TaskRecord task)
        {
            string[] lines = File.ReadAllLines(indexPath, Encoding.UTF8);
            if (task.IndexLineNumber < 0 || task.IndexLineNumber >= lines.Length)
            {
                throw new InvalidOperationException("Не удалось найти строку задачи в Index.md.");
            }

            lines[task.IndexLineNumber] = FormatIndexRow(task);
            File.WriteAllText(indexPath, string.Join("\n", lines), new UTF8Encoding(false));
        }

        private static void SaveTaskFile(string taskPath, TaskRecord task)
        {
            var lines = File.ReadAllLines(taskPath, Encoding.UTF8).ToList();
            string today = DateTime.Now.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);

            if (lines.Count == 0 || !string.Equals(lines[0].Trim(), "---", StringComparison.Ordinal))
            {
                var newLines = new List<string>
                {
                    "---",
                    "id: " + task.Id,
                    "title: " + (string.IsNullOrEmpty(task.Title) ? task.Id : task.Title),
                    "summary: " + EscapeFrontMatterValue(string.IsNullOrWhiteSpace(task.TaskSummary) ? task.IndexSummary : task.TaskSummary),
                    "priority: " + task.Priority,
                    "area: " + task.Area,
                    "owner: " + (string.IsNullOrEmpty(task.Owner) ? "unassigned" : task.Owner),
                    "created: " + (string.IsNullOrEmpty(task.Created) ? today : task.Created),
                    "updated: " + today,
                    "execution_time: " + task.ExecutionTime,
                    "depends_on: []",
                    "canonical_docs: []",
                    "related_files: []",
                    "---",
                    string.Empty,
                };
                newLines.AddRange(lines);
                lines = newLines;
            }

            int frontMatterEnd = -1;
            for (int i = 1; i < lines.Count; i++)
            {
                if (string.Equals(lines[i].Trim(), "---", StringComparison.Ordinal))
                {
                    frontMatterEnd = i;
                    break;
                }
            }

            if (frontMatterEnd < 0)
            {
                throw new InvalidOperationException("Не удалось прочитать front matter task-файла.");
            }

            UpsertFrontMatterField(lines, ref frontMatterEnd, "title", string.IsNullOrEmpty(task.Title) ? task.Id : task.Title);
            UpsertFrontMatterField(lines, ref frontMatterEnd, "summary", EscapeFrontMatterValue(string.IsNullOrWhiteSpace(task.TaskSummary) ? task.IndexSummary : task.TaskSummary));
            UpsertFrontMatterField(lines, ref frontMatterEnd, "priority", task.Priority);
            UpsertFrontMatterField(lines, ref frontMatterEnd, "area", task.Area);
            UpsertFrontMatterField(lines, ref frontMatterEnd, "owner", string.IsNullOrEmpty(task.Owner) ? "unassigned" : task.Owner);
            UpsertFrontMatterField(lines, ref frontMatterEnd, "created", string.IsNullOrEmpty(task.Created) ? today : task.Created);
            UpsertFrontMatterField(lines, ref frontMatterEnd, "updated", today);
            UpsertFrontMatterField(lines, ref frontMatterEnd, "execution_time", task.ExecutionTime);

            int bodyStart = frontMatterEnd + 1;
            while (bodyStart < lines.Count && string.IsNullOrWhiteSpace(lines[bodyStart]))
            {
                bodyStart++;
            }

            var sections = ParseBodySections(lines, bodyStart, task);
            sections.ValuesByHeading["Why"] = task.Why ?? string.Empty;
            sections.ValuesByHeading["Expected Outcome"] = task.ExpectedOutcome ?? string.Empty;
            sections.ValuesByHeading["Current Context"] = task.CurrentContext ?? string.Empty;
            sections.ValuesByHeading["Acceptance Criteria"] = task.AcceptanceCriteria ?? string.Empty;
            sections.ValuesByHeading["Verification"] = task.Verification ?? string.Empty;
            sections.ValuesByHeading["Risks / Open Questions"] = task.Risks ?? string.Empty;
            sections.ValuesByHeading["Human Decisions Needed"] = task.HumanDecisions ?? string.Empty;
            sections.ValuesByHeading["Decision Log"] = task.DecisionLog ?? string.Empty;
            sections.ValuesByHeading["Handoff Notes"] = task.HandoffNotes ?? string.Empty;

            string header = string.IsNullOrWhiteSpace(task.Title) ? task.Id : task.Id + " - " + task.Title;
            var rebuilt = new List<string>();
            rebuilt.AddRange(lines.Take(frontMatterEnd + 1));
            rebuilt.Add(string.Empty);
            rebuilt.Add("# " + header);
            rebuilt.Add(string.Empty);

            foreach (string heading in sections.OrderedHeadings)
            {
                if (!sections.ValuesByHeading.ContainsKey(heading))
                {
                    continue;
                }

                rebuilt.Add("## " + heading);
                rebuilt.Add(string.Empty);
                AddSectionContent(rebuilt, sections.ValuesByHeading[heading]);
                rebuilt.Add(string.Empty);
            }

            foreach (string heading in EditableSectionOrder)
            {
                if (sections.OrderedHeadings.Contains(heading))
                {
                    continue;
                }

                string content;
                if (!sections.ValuesByHeading.TryGetValue(heading, out content) || string.IsNullOrWhiteSpace(content))
                {
                    continue;
                }

                rebuilt.Add("## " + heading);
                rebuilt.Add(string.Empty);
                AddSectionContent(rebuilt, content);
                rebuilt.Add(string.Empty);
            }

            while (rebuilt.Count > 0 && string.IsNullOrWhiteSpace(rebuilt[rebuilt.Count - 1]))
            {
                rebuilt.RemoveAt(rebuilt.Count - 1);
            }

            File.WriteAllText(taskPath, string.Join("\n", rebuilt), new UTF8Encoding(false));
            task.Updated = today;
        }

        private static void UpsertFrontMatterField(List<string> lines, ref int frontMatterEnd, string key, string value)
        {
            string prefix = key + ":";
            for (int i = 1; i < frontMatterEnd; i++)
            {
                if (lines[i].StartsWith(prefix, StringComparison.Ordinal))
                {
                    lines[i] = key + ": " + value;
                    return;
                }
            }

            lines.Insert(frontMatterEnd, key + ": " + value);
            frontMatterEnd++;
        }

        private static string FormatIndexRow(TaskRecord task)
        {
            return string.Format(
                CultureInfo.InvariantCulture,
                "| {0} | {1} | {2} | {3} | {4} | {5} | `{6}` | {7} |",
                task.Id,
                task.Status,
                task.Priority,
                task.Area,
                string.IsNullOrWhiteSpace(task.Owner) ? "unassigned" : task.Owner,
                task.ExecutionTime,
                NormalizePath(task.RelativeFilePath),
                (task.IndexSummary ?? string.Empty).Replace("|", "/"));
        }

        private static void DeleteIndexRow(string indexPath, TaskRecord task)
        {
            string[] lines = File.ReadAllLines(indexPath, Encoding.UTF8);
            if (task.IndexLineNumber < 0 || task.IndexLineNumber >= lines.Length)
            {
                throw new InvalidOperationException("Не удалось найти строку задачи в Index.md.");
            }

            var updated = lines.Where((line, index) => index != task.IndexLineNumber).ToArray();
            File.WriteAllText(indexPath, string.Join("\n", updated), new UTF8Encoding(false));
        }

        private static string[] SplitMarkdownRow(string line)
        {
            string trimmed = line.Trim().Trim('|');
            return trimmed.Split('|').Select(part => part.Trim()).ToArray();
        }

        private static string StripTicks(string value)
        {
            return string.IsNullOrEmpty(value) ? string.Empty : value.Trim().Trim('`');
        }

        private static string ExtractTitleFromHeader(string header, string id)
        {
            if (string.IsNullOrEmpty(header))
            {
                return id;
            }

            string prefix = id + " - ";
            if (header.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
            {
                return header.Substring(prefix.Length).Trim();
            }

            return header.Trim();
        }

        private static string GetSection(Dictionary<string, StringBuilder> sections, string key)
        {
            StringBuilder value;
            if (!sections.TryGetValue(key, out value))
            {
                return string.Empty;
            }

            return value.ToString().Trim();
        }

        private static int CompareTaskIds(TaskRecord left, TaskRecord right)
        {
            return CompareTaskIds(left != null ? left.Id : null, right != null ? right.Id : null);
        }

        private static int CompareTaskIds(string left, string right)
        {
            return ExtractTaskNumber(left).CompareTo(ExtractTaskNumber(right));
        }

        private static int ExtractTaskNumber(string id)
        {
            if (string.IsNullOrEmpty(id))
            {
                return int.MaxValue;
            }

            string[] parts = id.Split('-');
            if (parts.Length < 2)
            {
                return int.MaxValue;
            }

            int number;
            return int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out number) ? number : int.MaxValue;
        }

        private static void LoadOwnerPresets(TaskBoardData data)
        {
            if (data == null)
            {
                return;
            }

            TaskBoardOwnersConfig config = EnsureOwnersConfig(data.OwnersConfigPath);
            data.OwnerPresets.Clear();

            if (config == null)
            {
                data.Warnings.Add("Не удалось загрузить owners config.");
                return;
            }

            foreach (string owner in config.owners)
            {
                if (!string.IsNullOrWhiteSpace(owner))
                {
                    data.OwnerPresets.Add(owner.Trim());
                }
            }

            if (!data.OwnerPresets.Any(owner => string.Equals(owner, "unassigned", StringComparison.OrdinalIgnoreCase)))
            {
                data.OwnerPresets.Insert(0, "unassigned");
            }
        }

        private static TaskBoardOwnersConfig EnsureOwnersConfig(string assetPath)
        {
            if (string.IsNullOrWhiteSpace(assetPath))
            {
                return null;
            }

            TaskBoardOwnersConfig config = AssetDatabase.LoadAssetAtPath<TaskBoardOwnersConfig>(assetPath);
            if (config != null)
            {
                return config;
            }

            string directory = Path.GetDirectoryName(assetPath);
            if (!string.IsNullOrEmpty(directory) && !AssetDatabase.IsValidFolder(directory))
            {
                EnsureAssetFolder(directory);
            }

            config = ScriptableObject.CreateInstance<TaskBoardOwnersConfig>();
            AssetDatabase.CreateAsset(config, assetPath);
            AssetDatabase.SaveAssets();
            AssetDatabase.Refresh();
            return config;
        }

        private static void EnsureAssetFolder(string assetFolder)
        {
            string normalized = NormalizePath(assetFolder);
            string[] parts = normalized.Split('/');
            string current = parts[0];

            for (int i = 1; i < parts.Length; i++)
            {
                string next = current + "/" + parts[i];
                if (!AssetDatabase.IsValidFolder(next))
                {
                    AssetDatabase.CreateFolder(current, parts[i]);
                }

                current = next;
            }
        }

        private static int ParseNumber(string value)
        {
            int number;
            return int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out number) ? number : 0;
        }

        private static string BuildCanonicalExecutionTime(int totalMinutes)
        {
            int remaining = totalMinutes;
            int weeks = remaining / TaskBoardConstants.MinutesPerWeek;
            remaining %= TaskBoardConstants.MinutesPerWeek;
            int days = remaining / TaskBoardConstants.MinutesPerDay;
            remaining %= TaskBoardConstants.MinutesPerDay;
            int hours = remaining / TaskBoardConstants.MinutesPerHour;
            remaining %= TaskBoardConstants.MinutesPerHour;
            int minutes = remaining;

            var builder = new StringBuilder();
            if (weeks > 0)
            {
                builder.Append(weeks).Append('w');
            }

            if (days > 0)
            {
                builder.Append(days).Append('d');
            }

            if (hours > 0)
            {
                builder.Append(hours).Append('h');
            }

            if (minutes > 0 || builder.Length == 0)
            {
                builder.Append(minutes).Append('m');
            }

            return builder.ToString();
        }

        private static string DeriveTitleFromPath(string relativeFilePath, string id)
        {
            if (string.IsNullOrWhiteSpace(relativeFilePath))
            {
                return id;
            }

            string fileName = Path.GetFileNameWithoutExtension(relativeFilePath) ?? id;
            string prefix = id + "-";
            if (fileName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
            {
                fileName = fileName.Substring(prefix.Length);
            }

            fileName = fileName.Replace('-', ' ').Trim();
            return string.IsNullOrWhiteSpace(fileName) ? id : fileName;
        }

        private static string EscapeFrontMatterValue(string value)
        {
            return string.IsNullOrWhiteSpace(value) ? string.Empty : value.Replace("\r", " ").Replace("\n", " ").Trim();
        }

        private static ParsedTaskBody ParseBodySections(List<string> lines, int bodyStart, TaskRecord task)
        {
            var parsed = new ParsedTaskBody();
            string currentHeading = null;

            for (int i = bodyStart; i < lines.Count; i++)
            {
                string line = lines[i];
                Match headingMatch = HeadingRegex.Match(line);
                if (headingMatch.Success)
                {
                    currentHeading = headingMatch.Groups[1].Value.Trim();
                    if (!parsed.OrderedHeadings.Contains(currentHeading))
                    {
                        parsed.OrderedHeadings.Add(currentHeading);
                    }

                    if (!parsed.ValuesByHeading.ContainsKey(currentHeading))
                    {
                        parsed.ValuesByHeading[currentHeading] = string.Empty;
                    }
                    continue;
                }

                if (H1Regex.IsMatch(line) || string.IsNullOrEmpty(currentHeading))
                {
                    continue;
                }

                if (parsed.ValuesByHeading[currentHeading].Length > 0)
                {
                    parsed.ValuesByHeading[currentHeading] += "\n";
                }

                parsed.ValuesByHeading[currentHeading] += line;
            }

            foreach (string heading in EditableSectionOrder)
            {
                if (!parsed.ValuesByHeading.ContainsKey(heading))
                {
                    parsed.ValuesByHeading[heading] = GetEditableSectionValue(task, heading);
                }
            }

            return parsed;
        }

        private static void AddSectionContent(List<string> lines, string content)
        {
            if (string.IsNullOrEmpty(content))
            {
                return;
            }

            lines.AddRange(content.Replace("\r\n", "\n").Split('\n'));
        }

        private static string GetEditableSectionValue(TaskRecord task, string heading)
        {
            switch (heading)
            {
                case "Why":
                    return task.Why ?? string.Empty;
                case "Expected Outcome":
                    return task.ExpectedOutcome ?? string.Empty;
                case "Current Context":
                    return task.CurrentContext ?? string.Empty;
                case "Acceptance Criteria":
                    return task.AcceptanceCriteria ?? string.Empty;
                case "Verification":
                    return task.Verification ?? string.Empty;
                case "Risks / Open Questions":
                    return task.Risks ?? string.Empty;
                case "Human Decisions Needed":
                    return task.HumanDecisions ?? string.Empty;
                case "Decision Log":
                    return task.DecisionLog ?? string.Empty;
                case "Handoff Notes":
                    return task.HandoffNotes ?? string.Empty;
                default:
                    return string.Empty;
            }
        }

        private sealed class ParsedTaskBody
        {
            public readonly List<string> OrderedHeadings = new List<string>();
            public readonly Dictionary<string, string> ValuesByHeading = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
        }
    }
}
