using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Newtonsoft.Json;
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium.Support.UI;

class Program
{
    static async Task Main(string[] args)
    {
        var options = new ChromeOptions();
        options.AddArgument("--headless");
        options.AddArgument("--no-sandbox");
        options.AddArgument("--disable-dev-shm-usage");
        options.AddArgument("--disable-gpu");
        options.AddArgument("--window-size=1920,1080");

        using var driver = new ChromeDriver(options);
        var scraper = new SpecialtyScraper(driver);

        try
        {
            var specialties = await scraper.ScrapeSpecialtiesAsync();
            await SaveResultsAsync(specialties);
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error occurred: {ex.Message}");
            File.WriteAllText("error_log.txt", ex.ToString());
        }
        finally
        {
            driver.Quit();
        }
    }

    private static async Task SaveResultsAsync(List<Specialty> specialties)
    {
        var json = JsonConvert.SerializeObject(specialties, Formatting.Indented);
        await File.WriteAllTextAsync("specialties.json", json);
        Console.WriteLine($"Saved {specialties.Count} specialties to specialties.json");
    }
}

class SpecialtyScraper
{
    private readonly IWebDriver _driver;
    private readonly WebDriverWait _wait;

    public SpecialtyScraper(IWebDriver driver)
    {
        _driver = driver;
        _wait = new WebDriverWait(driver, TimeSpan.FromSeconds(Config.WaitTimeoutSeconds));
    }

    public async Task<List<Specialty>> ScrapeSpecialtiesAsync()
    {
        await NavigateToPageAsync();
        return await ExtractSpecialtiesAsync();
    }

    private async Task NavigateToPageAsync()
    {
        _driver.Navigate().GoToUrl(Config.BaseUrl);
        await WaitForPageLoadAsync();
    }

    private async Task WaitForPageLoadAsync()
    {
        for (int attempt = 1; attempt <= Config.MaxRetryAttempts; attempt++)
        {
            try
            {
                _wait.Until(drv =>
                {
                    var elements = drv.FindElements(By.CssSelector(Config.CssSelectors.SpecialtyItem));
                    return elements != null && elements.Count > 0;
                });
                return;
            }
            catch (WebDriverTimeoutException) when (attempt < Config.MaxRetryAttempts)
            {
                Console.WriteLine($"Attempt {attempt} failed, retrying...");
                _driver.Navigate().Refresh();
                await Task.Delay(1000 * attempt); // Exponential backoff
            }
            catch (WebDriverException ex)
            {
                Console.WriteLine($"WebDriverException on attempt {attempt}: {ex.Message}");
                await Task.Delay(1000 * attempt);
            }
        }

        // Log the page source for debugging after all attempts fail
        File.WriteAllText("failed_page_source.html", _driver.PageSource);
        throw new WebDriverTimeoutException("Failed to load page after multiple attempts");
    }

    private async Task<List<Specialty>> ExtractSpecialtiesAsync()
    {
        var specialties = new List<Specialty>();
        var specialtyElements = _driver.FindElements(By.CssSelector(".specItem"));

        foreach (var element in specialtyElements)
        {
            try
            {
                var specialty = new Specialty
                {
                    Name = SafeFindElement(element, By.CssSelector(".specName a")),
                    Code = SafeFindElement(element, By.CssSelector(".specCode")),
                    Cost = SafeFindElement(element, By.XPath(".//div[contains(text(), 'Стоимость')]/following-sibling::center/a")),
                    BudgetPassingScore = SafeFindElement(element, By.XPath(".//div[contains(text(), 'Бюджет')]/following-sibling::center/a")),
                    BudgetPlaces = SafeFindElement(element, By.XPath(".//div[contains(text(), 'Бюджет')]/following-sibling::center/a[@style='font-size: 13px; color: gray;']")),
                    PaidPassingScore = SafeFindElement(element, By.XPath(".//div[contains(text(), 'Платное')]/following-sibling::center/a")),
                    PaidPlaces = SafeFindElement(element, By.XPath(".//div[contains(text(), 'Платное')]/following-sibling::center/a[@style='font-size: 13px; color: gray;']")),
                    ExamsRequired = ExtractExams(element)
                };

                if (specialty.IsValid())
                {
                    specialties.Add(specialty);
                    await Task.Delay(100); // Небольшая задержка для предотвращения перегрузки
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Error processing specialty: {ex.Message}");
            }
        }

        return specialties;
    }

    private string SafeFindElement(IWebElement parent, By selector, string defaultValue = "")
    {
        try
        {
            var element = parent.FindElement(selector);
            return element?.Text?.Trim() ?? defaultValue;
        }
        catch
        {
            return defaultValue;
        }
    }

    private List<string> ExtractExams(IWebElement element)
    {
        try
        {
            var exams = new List<string>();
            var examElements = element.FindElements(By.CssSelector(".examList li"));
            foreach (var exam in examElements)
            {
                exams.Add(exam.Text.Trim());
            }
            return exams;
        }
        catch
        {
            return new List<string>();
        }
    }
}

public class Specialty
{
    public string Name { get; set; } = string.Empty;
    public string Code { get; set; } = string.Empty;
    public string Cost { get; set; } = string.Empty;
    public string BudgetPassingScore { get; set; } = string.Empty;
    public string BudgetPlaces { get; set; } = string.Empty;
    public string PaidPassingScore { get; set; } = string.Empty;
    public string PaidPlaces { get; set; } = string.Empty;
    public List<string> ExamsRequired { get; set; } = new List<string>();

    public bool IsValid()
    {
        return !string.IsNullOrWhiteSpace(Name) && !string.IsNullOrWhiteSpace(Code);
    }

    public decimal? GetCostValue()
    {
        if (string.IsNullOrWhiteSpace(Cost))
            return null;

        // Удаляем все нечисловые символы, кроме точки
        string numericCost = new string(Cost.Where(c => char.IsDigit(c) || c == '.').ToArray());
        if (decimal.TryParse(numericCost, out decimal result))
            return result;

        return null;
    }
}

public static class Config
{
    public const int WaitTimeoutSeconds = 10;
    public const int MaxRetryAttempts = 3;
    public const string BaseUrl = "https://vuzopedia.ru/vuz/157/spec";

    public static class XPathSelectors
    {
        public const string Cost = ".//div[contains(text(), 'Стоимость')]/following-sibling::center/a";
        public const string BudgetScore = ".//div[contains(text(), 'Бюджет')]/following-sibling::center/a";
        public const string BudgetPlaces = ".//div[contains(text(), 'Бюджет')]/following-sibling::center/a[@style='font-size: 13px; color: gray;']";
        public const string PaidScore = ".//div[contains(text(), 'Платное')]/following-sibling::center/a";
        public const string PaidPlaces = ".//div[contains(text(), 'Платное')]/following-sibling::center/a[@style='font-size: 13px; color: gray;']";
        public const string Exams = ".examList li";
    }

    public static class CssSelectors
    {
        public const string SpecialtyItem = ".specItem";
        public const string SpecialtyName = ".specName a";
        public const string SpecialtyCode = ".specCode";
    }

    public static class OtherSelectors
    {
        // Add any other selectors if necessary
    }
}

Attempt 1 failed, retrying...
Attempt 2 failed, retrying...
WebDriverException on attempt 3: Timed out after 10 seconds
Error occurred: Failed to load page after multiple attempts