using System.Data;
using System.Diagnostics;
using System.Globalization;
using System.Text;

namespace AutorentWinForms;

public sealed class MainForm : Form
{
    private readonly Database _db;
    private readonly UserSession _session;
    private readonly TabControl _tabs = new() { Dock = DockStyle.Fill };
    private readonly Dictionary<string, DataGridView> _grids = new();
    private readonly Dictionary<string, TextBox> _searchBoxes = new();

    private bool CanWrite => _session.Role is "admin" or "manager";
    private bool CanEditBrands => _session.Role == "admin";
    private bool IsAdmin => _session.Role == "admin";

    public MainForm(Database db, UserSession session)
    {
        _db = db;
        _session = session;
        Text = $"Автопрокат - {_session.FullName} ({RoleName(_session.Role)})";
        StartPosition = FormStartPosition.CenterScreen;
        WindowState = FormWindowState.Maximized;
        MinimumSize = new Size(1100, 700);
        Font = new Font("Segoe UI", 10F);

        Controls.Add(_tabs);
        CreateDataTab("clients", "Клиенты");
        CreateDataTab("cars", "Автомобили");
        CreateDataTab("brands", "Марки");
        CreateDataTab("rentals", "Аренда");
        if (_session.Role != "viewer")
        {
            CreateReportsTab();
            CreateSecurityTab();
            CreateAuditTab();
        }

        LoadAll();
    }

    private void CreateDataTab(string table, string caption)
    {
        var page = new TabPage(caption);
        var root = new TableLayoutPanel { Dock = DockStyle.Fill, RowCount = 2, ColumnCount = 1 };
        root.RowStyles.Add(new RowStyle(SizeType.Absolute, 48));
        root.RowStyles.Add(new RowStyle(SizeType.Percent, 100));

        var bar = new FlowLayoutPanel { Dock = DockStyle.Fill, FlowDirection = FlowDirection.LeftToRight, Padding = new Padding(8), WrapContents = false };
        var search = new TextBox { Width = 320, PlaceholderText = "Поиск..." };
        search.TextChanged += (_, _) => LoadTable(table);
        _searchBoxes[table] = search;

        var add = new Button { Text = "Добавить", Width = 105, Height = 30, Enabled = CanWrite && (table != "brands" || CanEditBrands) };
        add.Click += (_, _) => EditRecord(table, null);
        var edit = new Button { Text = "Редактировать", Width = 125, Height = 30, Enabled = CanWrite && (table != "brands" || CanEditBrands) };
        edit.Click += (_, _) => EditSelected(table);
        var delete = new Button { Text = "Удалить", Width = 95, Height = 30, Enabled = CanWrite && (table != "brands" || CanEditBrands) };
        delete.Click += (_, _) => DeleteSelected(table);
        var refresh = new Button { Text = "Обновить", Width = 100, Height = 30 };
        refresh.Click += (_, _) => LoadTable(table);
        var export = new Button { Text = "CSV", Width = 70, Height = 30 };
        export.Click += (_, _) => ExportCsv(table);

        bar.Controls.AddRange([search, add, edit, delete, refresh, export]);

        var grid = new DataGridView
        {
            Dock = DockStyle.Fill,
            ReadOnly = true,
            AllowUserToAddRows = false,
            AllowUserToDeleteRows = false,
            SelectionMode = DataGridViewSelectionMode.FullRowSelect,
            MultiSelect = false,
            AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill,
            BackgroundColor = Color.White
        };
        grid.CellDoubleClick += (_, _) =>
        {
            if (CanWrite && (table != "brands" || CanEditBrands))
                EditSelected(table);
        };
        _grids[table] = grid;

        root.Controls.Add(bar, 0, 0);
        root.Controls.Add(grid, 0, 1);
        page.Controls.Add(root);
        _tabs.TabPages.Add(page);
    }

    private void CreateReportsTab()
    {
        var page = new TabPage("Отчеты");
        var root = new TableLayoutPanel { Dock = DockStyle.Fill, RowCount = 2, ColumnCount = 1 };
        root.RowStyles.Add(new RowStyle(SizeType.Absolute, 54));
        root.RowStyles.Add(new RowStyle(SizeType.Percent, 100));

        var bar = new FlowLayoutPanel { Dock = DockStyle.Fill, Padding = new Padding(8) };
        var refresh = new Button { Text = "Сформировать", Width = 130, Height = 32 };
        var html = new Button { Text = "Отчет HTML", Width = 120, Height = 32 };
        var txt = new Button { Text = "Отчет TXT", Width = 110, Height = 32 };
        refresh.Click += (_, _) => LoadReports();
        html.Click += (_, _) => SaveReport("html");
        txt.Click += (_, _) => SaveReport("txt");
        bar.Controls.AddRange([refresh, html, txt]);

        var report = new TextBox
        {
            Name = "reportBox",
            Dock = DockStyle.Fill,
            Multiline = true,
            ScrollBars = ScrollBars.Both,
            ReadOnly = true,
            Font = new Font("Consolas", 10F),
            WordWrap = false
        };

        root.Controls.Add(bar, 0, 0);
        root.Controls.Add(report, 0, 1);
        page.Controls.Add(root);
        _tabs.TabPages.Add(page);
    }

    private void CreateSecurityTab()
    {
        var page = new TabPage("Защита");
        page.BackColor = Color.FromArgb(245, 247, 250);

        var scroll = new Panel
        {
            Dock = DockStyle.Fill,
            AutoScroll = true,
            Padding = new Padding(18),
            BackColor = Color.FromArgb(245, 247, 250)
        };

        var content = new TableLayoutPanel
        {
            Dock = DockStyle.Top,
            AutoSize = true,
            ColumnCount = 1,
            BackColor = Color.Transparent
        };
        content.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));

        content.Controls.Add(CreateSectionTitle("Пользователи БД и обязанности"), 0, content.RowCount++);
        content.Controls.Add(CreateRolesPanel(), 0, content.RowCount++);
        content.Controls.Add(CreateSectionTitle("Реализованные уровни защиты"), 0, content.RowCount++);
        content.Controls.Add(CreateProtectionMatrix(), 0, content.RowCount++);
        content.Controls.Add(CreateSectionTitle("Защита на уровне базы данных"), 0, content.RowCount++);
        content.Controls.Add(CreateBulletsCard("Ограничения целостности и контроль данных", [
            "PRIMARY KEY: brand_id, car_id, client_id, rental_id исключают дублирование основных сущностей.",
            "FOREIGN KEY: car.brand_id, rental.car_id, rental.client_id защищают связи между марками, автомобилями, клиентами и договорами.",
            "ON UPDATE CASCADE сохраняет связи при изменении ключей, ON DELETE RESTRICT запрещает удаление используемых записей.",
            "UNIQUE: название марки, госномер, телефон клиента и номер водительского удостоверения не могут повторяться.",
            "CHECK: год выпуска 1990-2030, стоимость аренды больше 0, сумма договора не отрицательная, дата окончания не раньше даты начала.",
            "CHECK по статусам ограничивает значения: автомобиль может быть только «Свободен», «В аренде» или «На ТО», договор - «Активна» или «Завершена».",
            "Индексы idx_car_brand_id, idx_car_status, idx_rental_car_id, idx_rental_cli_id, idx_rental_dates, idx_rental_status ускоряют поиск и отчеты."
        ]), 0, content.RowCount++);
        content.Controls.Add(CreateBulletsCard("Представления для безопасной отчетности", [
            "v_rentals_full показывает договоры с ФИО клиента, телефоном, номером автомобиля, маркой, датами, суммой и статусом.",
            "v_available_cars ограничивает выборку только свободными автомобилями.",
            "v_client_stats агрегирует количество аренд и сумму затрат по клиентам.",
            "Отчеты приложения строятся по представлениям и агрегатным запросам, без ручной сборки данных пользователем."
        ]), 0, content.RowCount++);
        content.Controls.Add(CreateSectionTitle("Разграничение доступа"), 0, content.RowCount++);
        content.Controls.Add(CreateBulletsCard("Роли PostgreSQL из проекта", [
            "rent_admin - полный доступ ко всем таблицам и последовательностям.",
            "rent_manager - SELECT, INSERT, UPDATE для client, car, rental; SELECT по brand и отчетным VIEW.",
            "rent_viewer - только SELECT, подходит для аналитика и преподавательской проверки.",
            "Row Level Security включен для rental: политики разрешают полный доступ администратору и менеджеру, а просмотр можно ограничивать отдельной SELECT-политикой.",
            "В приложении дополнительно используются роли admin, manager, viewer в таблице app_user, чтобы управлять доступностью кнопок и операций интерфейса."
        ]), 0, content.RowCount++);
        content.Controls.Add(CreateSectionTitle("Защита на уровне приложения"), 0, content.RowCount++);
        content.Controls.Add(CreateBulletsCard("Меры, реализованные в Windows Forms", [
            "Авторизация по логину и паролю перед открытием главного окна.",
            "Пароли хранятся как PBKDF2-SHA256 хеш с индивидуальной солью, а не в открытом виде.",
            "Права интерфейса зависят от роли: viewer не может добавлять, редактировать и удалять записи.",
            "Все запросы выполняются через параметры Npgsql, что защищает от SQL-инъекций.",
            "Строка подключения вынесена в dbsettings.json или переменную AUTORENT_CONNECTION_STRING.",
            "dbsettings.json добавлен в .gitignore, чтобы пароль от БД не попадал в репозиторий.",
            "При запуске приложение синхронизирует SERIAL-последовательности с MAX(id), чтобы исключить duplicate key после ручного импорта данных."
        ]), 0, content.RowCount++);
        content.Controls.Add(CreateSectionTitle("Аудит операций"), 0, content.RowCount++);
        content.Controls.Add(CreateBulletsCard("Логирование изменений", [
            "Таблица audit_log хранит время операции, пользователя, действие, таблицу, ID записи и детали изменения.",
            "Триггеры AFTER INSERT OR UPDATE OR DELETE установлены на brand, car, client и rental.",
            "Текущий пользователь приложения сохраняется в app_context и попадает в журнал аудита.",
            "Отдельная вкладка «Журнал операций» показывает последние операции из audit_log.",
            "Такой аудит помогает доказать, кто и когда изменял автомобили, клиентов и договоры аренды."
        ]), 0, content.RowCount++);
        content.Controls.Add(CreateSqlCard(), 0, content.RowCount++);
        scroll.Controls.Add(content);
        page.Controls.Add(scroll);
        _tabs.TabPages.Add(page);
    }

    private static Label CreateSectionTitle(string text) => new()
    {
        Text = text,
        Dock = DockStyle.Top,
        Height = 34,
        ForeColor = Color.FromArgb(30, 41, 59),
        Font = new Font("Segoe UI", 13F, FontStyle.Bold),
        Margin = new Padding(0, 8, 0, 4)
    };

    private static Control CreateRolesPanel()
    {
        var layout = new TableLayoutPanel
        {
            Dock = DockStyle.Top,
            AutoSize = true,
            ColumnCount = 3,
            Margin = new Padding(0, 0, 0, 12)
        };
        layout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 33.33F));
        layout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 33.33F));
        layout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 33.33F));

        layout.Controls.Add(CreateRoleCard("admin", "Администратор", "Полный доступ", "Управляет структурой БД, справочниками, пользователями, аудитом, резервным копированием и контролем целостности.", Color.FromArgb(185, 28, 28)), 0, 0);
        layout.Controls.Add(CreateRoleCard("manager", "Менеджер проката", "Рабочий CRUD", "Вводит клиентов, автомобили и договоры аренды, редактирует рабочие данные, формирует отчеты.", Color.FromArgb(21, 91, 158)), 1, 0);
        layout.Controls.Add(CreateRoleCard("viewer", "Аналитик", "Только чтение", "Просматривает данные, строит отчеты и проверяет показатели без права изменения записей.", Color.FromArgb(22, 101, 52)), 2, 0);
        return layout;
    }

    private static Panel CreateRoleCard(string login, string title, string access, string description, Color accent)
    {
        var panel = CreateBaseCard(174);
        panel.Margin = new Padding(0, 0, 10, 0);
        panel.Padding = new Padding(14);

        var loginLabel = new Label
        {
            Text = login,
            ForeColor = accent,
            Font = new Font("Segoe UI", 11F, FontStyle.Bold),
            Dock = DockStyle.Top,
            Height = 26
        };
        var titleLabel = new Label
        {
            Text = title,
            ForeColor = Color.FromArgb(15, 23, 42),
            Font = new Font("Segoe UI", 12F, FontStyle.Bold),
            Dock = DockStyle.Top,
            Height = 30
        };
        var accessLabel = new Label
        {
            Text = access,
            ForeColor = Color.White,
            BackColor = accent,
            Font = new Font("Segoe UI", 9F, FontStyle.Bold),
            AutoSize = false,
            Height = 26,
            Dock = DockStyle.Top,
            TextAlign = ContentAlignment.MiddleCenter,
            Margin = new Padding(0, 0, 0, 8)
        };
        var body = new Label
        {
            Text = description,
            ForeColor = Color.FromArgb(71, 85, 105),
            Font = new Font("Segoe UI", 9.5F),
            Dock = DockStyle.Fill
        };

        panel.Controls.Add(body);
        panel.Controls.Add(accessLabel);
        panel.Controls.Add(titleLabel);
        panel.Controls.Add(loginLabel);
        return panel;
    }

    private static Control CreateProtectionMatrix()
    {
        var layout = new TableLayoutPanel
        {
            Dock = DockStyle.Top,
            AutoSize = true,
            ColumnCount = 2,
            Margin = new Padding(0, 0, 0, 12)
        };
        layout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50));
        layout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50));

        layout.Controls.Add(CreateBulletsCard("1. Идентификация и аутентификация", [
            "Пользователь входит по логину и паролю.",
            "Учетные записи хранятся в app_user.",
            "Пароль проверяется по хешу PBKDF2-SHA256 с солью."
        ]), 0, 0);
        layout.Controls.Add(CreateBulletsCard("2. Авторизация и роли", [
            "admin, manager, viewer управляют доступом в интерфейсе.",
            "rent_admin, rent_manager, rent_viewer описывают права на уровне PostgreSQL.",
            "Viewer не получает кнопки изменения данных."
        ]), 1, 0);
        layout.Controls.Add(CreateBulletsCard("3. Целостность данных", [
            "PK, FK, UNIQUE, CHECK защищают структуру БД.",
            "ON DELETE RESTRICT запрещает удалить запись, если она участвует в договоре.",
            "Последовательности синхронизируются после импорта."
        ]), 0, 1);
        layout.Controls.Add(CreateBulletsCard("4. Контроль и аудит", [
            "Триггеры фиксируют INSERT, UPDATE и DELETE.",
            "audit_log хранит пользователя, таблицу, действие и детали.",
            "Журнал доступен в отдельной вкладке приложения."
        ]), 1, 1);
        return layout;
    }

    private static Panel CreateBulletsCard(string title, string[] bullets)
    {
        var panel = CreateBaseCard(Math.Max(112, 48 + bullets.Length * 26));
        panel.Padding = new Padding(14);
        panel.Margin = new Padding(0, 0, 10, 12);

        var titleLabel = new Label
        {
            Text = title,
            Dock = DockStyle.Top,
            Height = 30,
            ForeColor = Color.FromArgb(15, 23, 42),
            Font = new Font("Segoe UI", 11F, FontStyle.Bold)
        };

        var body = new Label
        {
            Text = string.Join(Environment.NewLine, bullets.Select(x => "  - " + x)),
            Dock = DockStyle.Fill,
            ForeColor = Color.FromArgb(51, 65, 85),
            Font = new Font("Segoe UI", 9.6F),
            AutoEllipsis = false
        };

        panel.Controls.Add(body);
        panel.Controls.Add(titleLabel);
        return panel;
    }

    private static Panel CreateSqlCard()
    {
        var panel = CreateBaseCard(270);
        panel.Padding = new Padding(14);
        panel.Margin = new Padding(0, 0, 0, 12);

        var title = new Label
        {
            Text = "Ключевые SQL-механизмы защиты",
            Dock = DockStyle.Top,
            Height = 30,
            ForeColor = Color.FromArgb(15, 23, 42),
            Font = new Font("Segoe UI", 11F, FontStyle.Bold)
        };

        var sql = new RichTextBox
        {
            Dock = DockStyle.Fill,
            ReadOnly = true,
            BorderStyle = BorderStyle.None,
            BackColor = Color.FromArgb(248, 250, 252),
            ForeColor = Color.FromArgb(30, 41, 59),
            Font = new Font("Consolas", 9F),
            Text = """
                -- Ограничения целостности
                CONSTRAINT uq_client_phone UNIQUE,
                CONSTRAINT chk_rental_dates CHECK (end_date >= start_date),
                CONSTRAINT fk_rental_client REFERENCES client(client_id) ON DELETE RESTRICT

                -- Роли и права
                CREATE ROLE rent_admin NOLOGIN;
                CREATE ROLE rent_manager NOLOGIN;
                CREATE ROLE rent_viewer NOLOGIN;
                GRANT SELECT, INSERT, UPDATE ON client, car, rental TO rent_manager;
                GRANT SELECT ON ALL TABLES IN SCHEMA public TO rent_viewer;

                -- Row Level Security
                ALTER TABLE rental ENABLE ROW LEVEL SECURITY;
                CREATE POLICY manager_access ON rental
                  FOR ALL TO rent_manager USING (true) WITH CHECK (true);

                -- Аудит
                CREATE TRIGGER trg_audit_client
                AFTER INSERT OR UPDATE OR DELETE ON client
                FOR EACH ROW EXECUTE FUNCTION fn_autorent_audit('client_id');
                """
        };

        panel.Controls.Add(sql);
        panel.Controls.Add(title);
        return panel;
    }

    private static Panel CreateBaseCard(int height) => new()
    {
        Height = height,
        Dock = DockStyle.Top,
        BackColor = Color.White,
        BorderStyle = BorderStyle.FixedSingle
    };

    private void CreateAuditTab()
    {
        var page = new TabPage("Журнал операций");
        var root = new TableLayoutPanel { Dock = DockStyle.Fill, RowCount = 2 };
        root.RowStyles.Add(new RowStyle(SizeType.Absolute, 48));
        root.RowStyles.Add(new RowStyle(SizeType.Percent, 100));

        var bar = new FlowLayoutPanel { Dock = DockStyle.Fill, Padding = new Padding(8) };
        var refresh = new Button { Text = "Обновить", Width = 105, Height = 30 };
        refresh.Click += (_, _) => LoadAudit();
        bar.Controls.Add(refresh);

        var grid = new DataGridView
        {
            Dock = DockStyle.Fill,
            ReadOnly = true,
            AllowUserToAddRows = false,
            AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill,
            BackgroundColor = Color.White
        };
        _grids["audit"] = grid;

        root.Controls.Add(bar, 0, 0);
        root.Controls.Add(grid, 0, 1);
        page.Controls.Add(root);
        _tabs.TabPages.Add(page);
    }

    private void LoadAll()
    {
        foreach (var table in new[] { "clients", "cars", "brands", "rentals" })
            LoadTable(table);
        if (_session.Role != "viewer")
        {
            LoadReports();
            LoadAudit();
        }
    }

    private void LoadTable(string table)
    {
        var q = _searchBoxes.TryGetValue(table, out var box) ? box.Text.Trim() : "";
        var like = $"%{q}%";
        var data = table switch
        {
            "clients" => _db.Query("""
                SELECT client_id AS ID, full_name AS ФИО, phone AS Телефон, address AS Адрес,
                       license_no AS ВУ, date(created_at) AS Регистрация
                FROM client
                WHERE @q = '' OR full_name LIKE @like OR phone LIKE @like OR address LIKE @like OR license_no LIKE @like
                ORDER BY client_id DESC
                """, new() { ["@q"] = q, ["@like"] = like }),
            "cars" => _db.Query("""
                SELECT ca.car_id AS ID, ca.license_plate AS Госномер, b.name AS Марка, ca.year AS Год,
                       ca.daily_rate AS Цена_день, ca.status AS Статус
                FROM car ca JOIN brand b ON b.brand_id = ca.brand_id
                WHERE @q = '' OR ca.license_plate LIKE @like OR b.name LIKE @like OR ca.status LIKE @like
                ORDER BY ca.car_id DESC
                """, new() { ["@q"] = q, ["@like"] = like }),
            "brands" => _db.Query("""
                SELECT b.brand_id AS ID, b.name AS Марка, b.country AS Страна, COUNT(c.car_id) AS Автомобилей
                FROM brand b LEFT JOIN car c ON c.brand_id = b.brand_id
                WHERE @q = '' OR b.name LIKE @like OR b.country LIKE @like
                GROUP BY b.brand_id, b.name, b.country
                ORDER BY b.brand_id DESC
                """, new() { ["@q"] = q, ["@like"] = like }),
            "rentals" => _db.Query("""
                SELECT r.rental_id AS ID, c.full_name AS Клиент, ca.license_plate AS Авто, b.name AS Марка,
                       r.start_date AS Начало, r.end_date AS Конец, r.actual_return AS Возврат,
                       r.total_cost AS Сумма, r.status AS Статус
                FROM rental r
                JOIN client c ON c.client_id = r.client_id
                JOIN car ca ON ca.car_id = r.car_id
                JOIN brand b ON b.brand_id = ca.brand_id
                WHERE @q = '' OR c.full_name LIKE @like OR ca.license_plate LIKE @like OR b.name LIKE @like OR r.status LIKE @like
                ORDER BY r.rental_id DESC
                """, new() { ["@q"] = q, ["@like"] = like }),
            _ => new DataTable()
        };
        _grids[table].DataSource = data;
    }

    private void EditSelected(string table)
    {
        var row = _grids[table].CurrentRow;
        if (row?.Cells["ID"].Value is null) return;
        EditRecord(table, Convert.ToInt32(row.Cells["ID"].Value));
    }

    private void EditRecord(string table, int? id)
    {
        try
        {
            using var form = new RecordForm(_db, table, id);
            if (form.ShowDialog(this) != DialogResult.OK) return;
            LoadTable(table);
            LoadReports();
            LoadAudit();
        }
        catch (Exception ex)
        {
            MessageBox.Show(ex.Message, "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error);
        }
    }

    private void DeleteSelected(string table)
    {
        var row = _grids[table].CurrentRow;
        if (row?.Cells["ID"].Value is null) return;
        var id = Convert.ToInt32(row.Cells["ID"].Value);
        if (MessageBox.Show("Удалить выбранную запись?", "Подтверждение", MessageBoxButtons.YesNo, MessageBoxIcon.Warning) != DialogResult.Yes)
            return;

        try
        {
            var pk = table switch { "clients" => "client_id", "cars" => "car_id", "brands" => "brand_id", "rentals" => "rental_id", _ => "id" };
            var dbTable = table.TrimEnd('s');
            if (table == "clients") dbTable = "client";
            if (table == "cars") dbTable = "car";
            if (table == "brands") dbTable = "brand";
            if (table == "rentals") dbTable = "rental";
            _db.ExecuteNonQuery($"DELETE FROM {dbTable} WHERE {pk} = @id", new() { ["@id"] = id });
            LoadTable(table);
            LoadReports();
            LoadAudit();
        }
        catch (Exception ex)
        {
            MessageBox.Show("Удаление невозможно: запись используется в связанных таблицах или нарушает ограничения.\n\n" + ex.Message,
                "Защита целостности", MessageBoxButtons.OK, MessageBoxIcon.Warning);
        }
    }

    private void LoadAudit()
    {
        _grids["audit"].DataSource = _db.Query("""
            SELECT audit_id AS ID, changed_at AS Время, user_login AS Пользователь,
                   action AS Операция, entity AS Таблица, entity_id AS Запись, details AS Детали
            FROM audit_log
            ORDER BY audit_id DESC
            LIMIT 300
            """);
    }

    private void LoadReports()
    {
        var box = FindReportBox();
        box.Text = BuildReportText();
    }

    private string BuildReportText()
    {
        var stats = _db.Query("""
            SELECT
              (SELECT COUNT(*) FROM client) AS clients,
              (SELECT COUNT(*) FROM car) AS cars,
              (SELECT COUNT(*) FROM rental WHERE status = 'Активна') AS active_rentals,
              (SELECT COALESCE(SUM(total_cost),0) FROM rental WHERE status = 'Завершена') AS revenue
            """).Rows[0];
        var sb = new StringBuilder();
        sb.AppendLine("ОТЧЕТ ПО СИСТЕМЕ АВТОПРОКАТА");
        sb.AppendLine($"Дата формирования: {DateTime.Now:dd.MM.yyyy HH:mm}");
        sb.AppendLine($"Пользователь: {_session.FullName} ({RoleName(_session.Role)})");
        sb.AppendLine(new string('=', 72));
        sb.AppendLine($"Клиентов: {stats["clients"]}");
        sb.AppendLine($"Автомобилей: {stats["cars"]}");
        sb.AppendLine($"Активных аренд: {stats["active_rentals"]}");
        sb.AppendLine($"Выручка по завершенным арендам: {Convert.ToDecimal(stats["revenue"]).ToString("N2", CultureInfo.GetCultureInfo("ru-RU"))} руб.");
        sb.AppendLine();

        AppendTable(sb, "Активные договоры", _db.Query("SELECT * FROM v_rentals_full WHERE status='Активна' ORDER BY start_date DESC"));
        AppendTable(sb, "Статистика по клиентам", _db.Query("SELECT full_name, total_rentals, total_spent FROM v_client_stats ORDER BY total_spent DESC"));
        AppendTable(sb, "Свободные автомобили", _db.Query("SELECT * FROM v_available_cars ORDER BY daily_rate DESC"));
        return sb.ToString();
    }

    private static void AppendTable(StringBuilder sb, string title, DataTable table)
    {
        sb.AppendLine(title);
        sb.AppendLine(new string('-', title.Length));
        if (table.Rows.Count == 0)
        {
            sb.AppendLine("Нет данных.");
            sb.AppendLine();
            return;
        }

        sb.AppendLine(string.Join(" | ", table.Columns.Cast<DataColumn>().Select(c => c.ColumnName)));
        foreach (DataRow row in table.Rows)
            sb.AppendLine(string.Join(" | ", row.ItemArray.Select(v => Convert.ToString(v))));
        sb.AppendLine();
    }

    private void SaveReport(string format)
    {
        var reportsDir = Path.Combine(AppContext.BaseDirectory, "Reports");
        Directory.CreateDirectory(reportsDir);
        var path = Path.Combine(reportsDir, $"autorent_report_{DateTime.Now:yyyyMMdd_HHmmss}.{format}");
        var text = BuildReportText();

        if (format == "html")
        {
            var html = "<!doctype html><meta charset=\"utf-8\"><title>Отчет Автопрокат</title>" +
                       "<style>body{font-family:Segoe UI,Arial;margin:32px}pre{white-space:pre-wrap;font:14px Consolas}</style>" +
                       "<h1>Отчет по системе Автопрокат</h1><pre>" + System.Net.WebUtility.HtmlEncode(text) + "</pre>";
            File.WriteAllText(path, html, Encoding.UTF8);
        }
        else
        {
            File.WriteAllText(path, text, Encoding.UTF8);
        }

        MessageBox.Show($"Отчет сохранен:\n{path}", "Готово", MessageBoxButtons.OK, MessageBoxIcon.Information);
        try { Process.Start(new ProcessStartInfo(path) { UseShellExecute = true }); } catch { }
    }

    private void ExportCsv(string table)
    {
        if (_grids[table].DataSource is not DataTable data) return;
        var reportsDir = Path.Combine(AppContext.BaseDirectory, "Reports");
        Directory.CreateDirectory(reportsDir);
        var path = Path.Combine(reportsDir, $"{table}_{DateTime.Now:yyyyMMdd_HHmmss}.csv");
        var sb = new StringBuilder();
        sb.AppendLine(string.Join(";", data.Columns.Cast<DataColumn>().Select(c => EscapeCsv(c.ColumnName))));
        foreach (DataRow row in data.Rows)
            sb.AppendLine(string.Join(";", row.ItemArray.Select(v => EscapeCsv(Convert.ToString(v) ?? ""))));
        File.WriteAllText(path, sb.ToString(), new UTF8Encoding(true));
        MessageBox.Show($"CSV сохранен:\n{path}", "Экспорт", MessageBoxButtons.OK, MessageBoxIcon.Information);
    }

    private static string EscapeCsv(string value) => "\"" + value.Replace("\"", "\"\"") + "\"";

    private TextBox FindReportBox()
    {
        return _tabs.TabPages.Cast<TabPage>().SelectMany(p => p.Controls.Find("reportBox", true)).OfType<TextBox>().First();
    }

    private static string RoleName(string role) => role switch
    {
        "admin" => "Администратор",
        "manager" => "Менеджер",
        "viewer" => "Аналитик",
        _ => role
    };

    private string SecurityText() => """
        Пользователи и обязанности

        1. admin / Администратор БД
           Полный доступ к справочникам, автомобилям, клиентам, договорам, отчетам и журналу аудита.
           Отвечает за структуру БД, учетные записи, резервное копирование и контроль целостности.

        2. manager / Менеджер проката
           Вводит клиентов, автомобили и договоры аренды, редактирует рабочие данные, формирует отчеты.
           Не управляет ролями и справочником марок без администратора.

        3. viewer / Аналитик
           Имеет доступ только к просмотру данных и формированию отчетов. Изменение и удаление записей запрещено.

        Защита на уровне приложения

        - вход по логину и паролю;
        - хранение паролей в виде PBKDF2-SHA256 хеша с индивидуальной солью;
        - разграничение прав по ролям admin, manager, viewer;
        - запрет кнопок добавления, редактирования и удаления для роли viewer;
        - параметризованные SQL-запросы вместо конкатенации пользовательского ввода;
        - обработка ошибок ограничений БД без раскрытия лишней служебной информации пользователю.

        Защита на уровне базы данных

        - первичные и внешние ключи;
        - UNIQUE для госномера, телефона, водительского удостоверения и названия марки;
        - CHECK для статусов, положительной цены, корректного года выпуска и дат аренды;
        - ON DELETE RESTRICT для защиты связанных договоров от случайного удаления;
        - представления v_rentals_full, v_available_cars, v_client_stats для отчетности;
        - триггеры INSERT/UPDATE/DELETE на основных таблицах пишут операции в audit_log;
        - PRAGMA foreign_keys = ON и WAL-журналирование при работе приложения.
        """;
}

internal sealed class RecordForm : Form
{
    private readonly Database _db;
    private readonly string _table;
    private readonly int? _id;
    private readonly Dictionary<string, Control> _fields = new();

    public RecordForm(Database db, string table, int? id)
    {
        _db = db;
        _table = table;
        _id = id;
        Text = id is null ? "Добавить запись" : "Редактировать запись";
        StartPosition = FormStartPosition.CenterParent;
        FormBorderStyle = FormBorderStyle.FixedDialog;
        MaximizeBox = false;
        MinimizeBox = false;
        ClientSize = new Size(520, table == "rentals" ? 430 : 350);
        Font = new Font("Segoe UI", 10F);

        BuildForm();
    }

    private void BuildForm()
    {
        var panel = new TableLayoutPanel { Dock = DockStyle.Fill, Padding = new Padding(16), ColumnCount = 2, AutoScroll = true };
        panel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 170));
        panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));

        var data = _id is null ? null : LoadRecord();
        foreach (var field in Fields())
            AddField(panel, field, data);

        var buttons = new FlowLayoutPanel { FlowDirection = FlowDirection.RightToLeft, Dock = DockStyle.Fill };
        var save = new Button { Text = "Сохранить", Width = 110, Height = 32 };
        var cancel = new Button { Text = "Отмена", Width = 90, Height = 32 };
        save.Click += (_, _) => Save();
        cancel.Click += (_, _) => Close();
        buttons.Controls.AddRange([save, cancel]);
        panel.Controls.Add(new Label(), 0, panel.RowCount);
        panel.Controls.Add(buttons, 1, panel.RowCount++);
        Controls.Add(panel);
        AcceptButton = save;
    }

    private DataRow LoadRecord()
    {
        var (dbTable, pk) = DbTable();
        return _db.Query($"SELECT * FROM {dbTable} WHERE {pk}=@id", new() { ["@id"] = _id }).Rows[0];
    }

    private IEnumerable<(string Name, string Label, string Type)> Fields() => _table switch
    {
        "clients" => [("full_name", "ФИО", "text"), ("phone", "Телефон", "text"), ("address", "Адрес", "text"), ("license_no", "Водительское удостоверение", "text")],
        "cars" => [("license_plate", "Госномер", "text"), ("brand_id", "Марка", "brand"), ("year", "Год выпуска", "number"), ("daily_rate", "Цена за день", "money"), ("status", "Статус", "car_status")],
        "brands" => [("name", "Марка", "text"), ("country", "Страна", "text")],
        "rentals" => [("client_id", "Клиент", "client"), ("car_id", "Автомобиль", "car"), ("start_date", "Дата начала", "date"), ("end_date", "Дата окончания", "date"), ("actual_return", "Фактический возврат", "date"), ("total_cost", "Сумма", "money"), ("status", "Статус", "rental_status")],
        _ => []
    };

    private void AddField(TableLayoutPanel panel, (string Name, string Label, string Type) field, DataRow? data)
    {
        panel.RowStyles.Add(new RowStyle(SizeType.Absolute, 42));
        panel.Controls.Add(new Label { Text = field.Label, AutoSize = true, Anchor = AnchorStyles.Left }, 0, panel.RowCount);

        Control input = field.Type switch
        {
            "brand" => Combo("SELECT brand_id, name FROM brand ORDER BY name", "brand_id", data),
            "client" => Combo("SELECT client_id, full_name FROM client ORDER BY full_name", "client_id", data),
            "car" => Combo("SELECT car_id, license_plate || ' - ' || (SELECT name FROM brand WHERE brand_id=car.brand_id) FROM car ORDER BY license_plate", "car_id", data),
            "car_status" => ComboValues(["Свободен", "В аренде", "На ТО"], field.Name, data),
            "rental_status" => ComboValues(["Активна", "Завершена"], field.Name, data),
            "date" => new DateTimePicker { Width = 280, Format = DateTimePickerFormat.Short, ShowCheckBox = field.Name == "actual_return" },
            _ => new TextBox { Width = 280 }
        };

        if (input is TextBox tb)
            tb.Text = data is null ? "" : Convert.ToString(data[field.Name]);
        if (input is DateTimePicker dtp)
        {
            var value = data is null ? "" : Convert.ToString(data[field.Name]);
            if (DateTime.TryParse(value, out var date))
            {
                dtp.Value = date;
                dtp.Checked = true;
            }
            else
            {
                dtp.Value = DateTime.Today;
                dtp.Checked = field.Name != "actual_return";
            }
        }

        input.Anchor = AnchorStyles.Left;
        _fields[field.Name] = input;
        panel.Controls.Add(input, 1, panel.RowCount++);
    }

    private ComboBox Combo(string sql, string fieldName, DataRow? data)
    {
        var combo = new ComboBox { Width = 280, DropDownStyle = ComboBoxStyle.DropDownList };
        var table = _db.Query(sql);
        combo.DataSource = table;
        combo.ValueMember = table.Columns[0].ColumnName;
        combo.DisplayMember = table.Columns[1].ColumnName;
        if (data is not null && data[fieldName] != DBNull.Value)
            combo.SelectedValue = Convert.ToInt32(data[fieldName]);
        return combo;
    }

    private static ComboBox ComboValues(string[] values, string fieldName, DataRow? data)
    {
        var combo = new ComboBox { Width = 280, DropDownStyle = ComboBoxStyle.DropDownList };
        combo.Items.AddRange(values);
        combo.SelectedItem = data is null ? values[0] : Convert.ToString(data[fieldName]);
        return combo;
    }

    private void Save()
    {
        try
        {
            ValidateRequired();
            var (dbTable, pk) = DbTable();
            var fields = Fields().Select(f => f.Name).ToArray();
            var values = fields.ToDictionary(f => "@" + f, ReadValue);

            if (_id is null)
            {
                var sql = $"INSERT INTO {dbTable}({string.Join(",", fields)}) VALUES({string.Join(",", values.Keys)})";
                _db.ExecuteNonQuery(sql, values);
            }
            else
            {
                values["@id"] = _id;
                var set = string.Join(",", fields.Select(f => $"{f}=@{f}"));
                _db.ExecuteNonQuery($"UPDATE {dbTable} SET {set} WHERE {pk}=@id", values);
            }

            DialogResult = DialogResult.OK;
            Close();
        }
        catch (Exception ex)
        {
            MessageBox.Show(ex.Message, "Ошибка сохранения", MessageBoxButtons.OK, MessageBoxIcon.Error);
        }
    }

    private void ValidateRequired()
    {
        foreach (var field in Fields())
        {
            if (_fields[field.Name] is TextBox tb && string.IsNullOrWhiteSpace(tb.Text) && field.Name is "full_name" or "license_plate" or "name")
                throw new InvalidOperationException($"Заполните поле «{field.Label}».");
        }
    }

    private object? ReadValue(string parameterName)
    {
        var name = parameterName.TrimStart('@');
        var control = _fields[name];
        return control switch
        {
            TextBox tb when name is "year" => int.TryParse(tb.Text, out var i) ? i : null,
            TextBox tb when name is "daily_rate" or "total_cost" => decimal.TryParse(tb.Text.Replace(',', '.'), NumberStyles.Any, CultureInfo.InvariantCulture, out var d) ? d : null,
            TextBox tb => string.IsNullOrWhiteSpace(tb.Text) ? null : tb.Text.Trim(),
            ComboBox cb => cb.SelectedValue is DataRowView ? cb.Text : cb.SelectedValue ?? cb.SelectedItem,
            DateTimePicker dtp => dtp.Checked ? dtp.Value.ToString("yyyy-MM-dd") : null,
            _ => null
        };
    }

    private (string Table, string Pk) DbTable() => _table switch
    {
        "clients" => ("client", "client_id"),
        "cars" => ("car", "car_id"),
        "brands" => ("brand", "brand_id"),
        "rentals" => ("rental", "rental_id"),
        _ => throw new InvalidOperationException("Неизвестная таблица.")
    };
}
