using Microsoft.Win32;
using QRCoder;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing.Drawing2D;
using System.Text.Json;
using System.Text.Json.Nodes;

namespace CodeRemoteManager.SetupApp;

public sealed class MainForm : Form
{
    private enum UiLanguage { English, Korean }
    private UiLanguage language = UiLanguage.English;
    private static readonly Color Accent = Color.FromArgb(0, 103, 192);
    private static readonly Color Success = Color.FromArgb(18, 135, 62);
    private static readonly Color Danger = Color.FromArgb(185, 38, 38);
    private static readonly Color Muted = Color.FromArgb(92, 92, 92);

    private readonly Panel content = new() { Dock = DockStyle.Fill, Padding = new Padding(40, 18, 40, 14) };
    private readonly Label title = new() { AutoSize = true, Font = new Font("Segoe UI Semibold", 24), ForeColor = Color.FromArgb(28, 28, 28) };
    private readonly Label subtitle = new() { AutoSize = true, Font = new Font("Segoe UI", 10.5f), ForeColor = Muted, MaximumSize = new Size(680, 0), Margin = new Padding(0, 7, 0, 0) };
    private readonly StepProgress steps = new() { Dock = DockStyle.Fill };
    private readonly Label codexStatus = StatusLabel();
    private readonly Label tailscaleStatus = StatusLabel();
    private readonly Button codexAction = SecondaryButton("Install guide");
    private readonly Button tailscaleAction = SecondaryButton("Install guide");
    private readonly TextBox projectPath = new() { Dock = DockStyle.Fill, Font = new Font("Segoe UI", 10), ReadOnly = true, BorderStyle = BorderStyle.FixedSingle };
    private readonly TextBox address = new() { Dock = DockStyle.Fill, ReadOnly = true, Font = new Font("Segoe UI", 10), BorderStyle = BorderStyle.FixedSingle };
    private readonly PictureBox qr = new() { Dock = DockStyle.Fill, SizeMode = PictureBoxSizeMode.Zoom, Margin = new Padding(0) };
    private readonly Label pairingCode = new() { AutoSize = true, Font = new Font("Consolas", 17, FontStyle.Bold), ForeColor = Color.FromArgb(28, 28, 28), Margin = new Padding(0, 8, 0, 2) };
    private readonly Label pairingExpiry = new() { AutoSize = true, ForeColor = Muted, Margin = new Padding(0, 0, 0, 8) };
    private readonly Button pairingAction = PrimaryButton("Pair a device");
    private readonly System.Windows.Forms.Timer pairingTimer = new() { Interval = 1000 };
    private readonly Label result = new() { AutoSize = true, ForeColor = Danger, MaximumSize = new Size(640, 0), Margin = new Padding(0, 8, 0, 0) };
    private readonly Button backButton = SecondaryButton("Back");
    private readonly Button nextButton = PrimaryButton("Next");
    private readonly ComboBox languagePicker = new() { DropDownStyle = ComboBoxStyle.DropDownList, Width = 104 };
    private readonly string configurationPath;
    private readonly bool hasExistingConfiguration;
    private int currentStep;
    private bool hostPrepared;
    private DateTimeOffset? pairingExpiresAt;
    private string? activePairingCode;
    private string? codexPath;
    private string? tailscalePath;
    private string? tailnetUrl;

    public MainForm()
    {
        var localDataRoot = Path.Combine(
            Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
            "CodeRemoteManager");
        configurationPath = Path.Combine(localDataRoot, "config.local.json");
        hasExistingConfiguration = File.Exists(configurationPath);
        Text = "Code Remote Manager PC Setup";
        ClientSize = new Size(780, 620);
        MinimumSize = new Size(720, 590);
        MaximumSize = new Size(980, 760);
        StartPosition = FormStartPosition.CenterScreen;
        BackColor = Color.White;
        Font = new Font("Segoe UI", 10);
        AutoScaleMode = AutoScaleMode.Dpi;

        codexAction.Click += OpenCodex;
        tailscaleAction.Click += OpenTailscale;
        pairingAction.Click += StartPairing;
        pairingTimer.Tick += (_, _) => UpdatePairingDisplay();
        backButton.Click += (_, _) => ShowStep(currentStep - 1);
        nextButton.Click += Next;
        languagePicker.Items.AddRange(["English", "한국어"]);
        languagePicker.SelectedIndex = 0;
        languagePicker.SelectedIndexChanged += (_, _) => { language = languagePicker.SelectedIndex == 1 ? UiLanguage.Korean : UiLanguage.English; ApplyLanguage(); };

        var shell = new TableLayoutPanel { Dock = DockStyle.Fill, ColumnCount = 1, RowCount = 4, BackColor = Color.White };
        shell.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
        shell.RowStyles.Add(new RowStyle(SizeType.Absolute, 122));
        shell.RowStyles.Add(new RowStyle(SizeType.Absolute, 84));
        shell.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
        shell.RowStyles.Add(new RowStyle(SizeType.Absolute, 72));
        shell.Controls.Add(BuildHeader(), 0, 0);
        shell.Controls.Add(BuildSteps(), 0, 1);
        shell.Controls.Add(content, 0, 2);
        shell.Controls.Add(BuildFooter(), 0, 3);
        Controls.Add(shell);

        projectPath.Text = hasExistingConfiguration ? string.Empty : Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
        projectPath.PlaceholderText = hasExistingConfiguration
            ? L("Keep existing workspaces", "기존 작업 영역 유지")
            : string.Empty;
        Shown += (_, _) => { RefreshStatus(); ShowStep(0); };
        FormClosed += (_, _) => { pairingTimer.Stop(); qr.Image?.Dispose(); };
    }

    private Control BuildHeader()
    {
        var panel = new TableLayoutPanel { Dock = DockStyle.Fill, Padding = new Padding(40, 28, 40, 0), ColumnCount = 2, RowCount = 2 };
        panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
        panel.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
        panel.Controls.Add(title, 0, 0);
        panel.Controls.Add(languagePicker, 1, 0);
        panel.Controls.Add(subtitle, 0, 1);
        panel.SetColumnSpan(subtitle, 2);
        return panel;
    }

    private string L(string english, string korean) => language == UiLanguage.English ? english : korean;

    private void ApplyLanguage()
    {
        Text = L("Code Remote Manager PC Setup", "Code Remote Manager PC 설정");
        backButton.Text = L("Back", "이전");
        steps.Labels = language == UiLanguage.English
            ? ["Requirements", "Project folder", "Phone connection"]
            : ["필수 프로그램", "프로젝트 폴더", "휴대폰 연결"];
        RefreshStatus();
        ShowStep(currentStep);
    }

    private Control BuildSteps()
    {
        var panel = new Panel { Dock = DockStyle.Fill, Padding = new Padding(40, 4, 40, 4) };
        panel.Controls.Add(steps);
        return panel;
    }

    private Control BuildFooter()
    {
        var footer = new TableLayoutPanel { Dock = DockStyle.Fill, Padding = new Padding(40, 14, 40, 14), ColumnCount = 3, BackColor = Color.FromArgb(248, 248, 248) };
        footer.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
        footer.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
        footer.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
        footer.Controls.Add(backButton, 0, 0);
        footer.Controls.Add(nextButton, 2, 0);
        return footer;
    }

    private void ShowStep(int step)
    {
        currentStep = Math.Clamp(step, 0, 2);
        result.Text = "";
        content.Controls.Clear();
        steps.CurrentStep = currentStep;
        steps.Invalidate();
        backButton.Visible = currentStep > 0;
        nextButton.Text = currentStep == 2 ? L("Finish setup", "설정 완료") : L("Next", "다음");

        if (currentStep == 0)
        {
            title.Text = L("Prepare your PC", "PC 연결 준비");
            subtitle.Text = L("Check the programs required to connect your phone.", "휴대폰 연결에 필요한 프로그램이 준비되었는지 확인합니다.");
            content.Controls.Add(BuildPrerequisites());
        }
        else if (currentStep == 1)
        {
            title.Text = L("Choose a project folder", "프로젝트 폴더 선택");
            subtitle.Text = hasExistingConfiguration
                ? L("Keep the existing workspaces, or choose another folder to add.", "기존 작업 영역을 유지하거나 추가할 폴더를 선택하세요.")
                : L("Choose the folder Codex can work in. Only files inside this folder will be accessible.", "Codex가 작업할 폴더를 선택하세요. 선택한 폴더 안의 파일만 접근할 수 있습니다.");
            content.Controls.Add(BuildProject());
        }
        else
        {
            RefreshStatus();
            title.Text = L("Connect your phone", "휴대폰 연결");
            subtitle.Text = L("Create a five-minute code, then scan it with Code Remote Manager on your phone.", "5분짜리 코드를 만든 뒤 휴대폰의 Code Remote Manager로 스캔하세요.");
            content.Controls.Add(BuildConnection());
            UpdatePairingDisplay();
        }
    }

    private Control BuildPrerequisites()
    {
        var stack = Stack();
        stack.Controls.Add(StatusCard("Codex CLI", codexStatus, codexAction));
        stack.Controls.Add(StatusCard("Tailscale", tailscaleStatus, tailscaleAction));
        var refresh = SecondaryButton(L("Check again", "다시 확인"));
        refresh.Click += (_, _) => { RefreshStatus(); ShowStep(0); };
        refresh.Visible = codexPath is null || tailnetUrl is null;
        var row = new FlowLayoutPanel { AutoSize = true, FlowDirection = FlowDirection.LeftToRight, Margin = new Padding(0, 8, 0, 0) };
        row.Controls.Add(refresh);
        row.Controls.Add(result);
        if (refresh.Visible) stack.Controls.Add(row);
        else stack.Controls.Add(Info(L("✓  All required programs are ready.", "✓  필수 프로그램이 모두 준비되었습니다.")));
        return stack;
    }

    private Control BuildProject()
    {
        var card = new RoundedPanel { Dock = DockStyle.Top, Height = 178, Padding = new Padding(22), BackColor = Color.FromArgb(250, 250, 250), BorderColor = Color.FromArgb(220, 220, 220), Radius = 12, Margin = new Padding(0) };
        var grid = new TableLayoutPanel { Dock = DockStyle.Fill, ColumnCount = 2, BackColor = Color.Transparent };
        grid.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 76));
        grid.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
        grid.Controls.Add(new IconBadge(IconKind.Folder) { Dock = DockStyle.Top, Margin = new Padding(0, 4, 16, 0) }, 0, 0);
        var details = new Panel { Dock = DockStyle.Fill, BackColor = Color.Transparent };
        var heading = Heading(L("Project folder", "프로젝트 폴더"));
        var picker = SecondaryButton(L("Choose folder", "폴더 선택"));
        picker.Click += SelectProject;
        var row = new TableLayoutPanel { Dock = DockStyle.Top, Height = 48, ColumnCount = 2, Margin = new Padding(0, 14, 0, 12) };
        row.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
        row.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
        projectPath.Margin = new Padding(0, 5, 10, 5);
        row.Controls.Add(projectPath, 0, 0);
        row.Controls.Add(picker, 1, 0);
        details.Controls.Add(Info(hasExistingConfiguration
            ? L("ⓘ  Leave this empty to preserve every existing workspace.", "ⓘ  비워 두면 기존 작업 영역을 모두 유지합니다.")
            : L("ⓘ  The selected folder will be registered as a safe workspace.", "ⓘ  선택한 폴더가 안전한 작업 영역으로 등록됩니다.")));
        details.Controls.Add(row);
        details.Controls.Add(heading);
        grid.Controls.Add(details, 1, 0);
        card.Controls.Add(grid);
        var stack = Stack();
        stack.Controls.Add(card);
        stack.Controls.Add(result);
        return stack;
    }

    private Control BuildConnection()
    {
        var card = new RoundedPanel { Dock = DockStyle.Top, Height = 300, Padding = new Padding(22), BackColor = Color.FromArgb(250, 250, 250), BorderColor = Color.FromArgb(220, 220, 220), Radius = 12, Margin = new Padding(0) };
        var cardGrid = new TableLayoutPanel { Dock = DockStyle.Fill, ColumnCount = 2, BackColor = Color.Transparent };
        cardGrid.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 230));
        cardGrid.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
        qr.BackColor = Color.White;
        cardGrid.Controls.Add(qr, 0, 0);

        var details = new TableLayoutPanel { Dock = DockStyle.Fill, Padding = new Padding(26, 2, 0, 0), ColumnCount = 1, RowCount = 7 };
        details.Controls.Add(Heading(L("Pair a device", "\uAE30\uAE30 \uC5F0\uACB0")));
        details.Controls.Add(new Label
        {
            Text = L("Use the QR or code only on a device you own.", "\uBCF8\uC778\uC774 \uC18C\uC720\uD55C \uAE30\uAE30\uC5D0\uC11C\uB9CC QR\uACFC \uCF54\uB4DC\uB97C \uC0AC\uC6A9\uD558\uC138\uC694."),
            AutoSize = true,
            MaximumSize = new Size(360, 0),
            ForeColor = Muted,
            Margin = new Padding(0, 6, 0, 10)
        });
        pairingAction.MinimumSize = new Size(170, 40);
        pairingAction.Dock = DockStyle.Top;
        details.Controls.Add(pairingAction);
        details.Controls.Add(pairingCode);
        details.Controls.Add(pairingExpiry);
        details.Controls.Add(new Label { Text = L("Connection address", "\uC5F0\uACB0 \uC8FC\uC18C"), AutoSize = true, ForeColor = Muted });
        address.Margin = new Padding(0, 5, 0, 0);
        details.Controls.Add(address);

        cardGrid.Controls.Add(details, 1, 0);
        card.Controls.Add(cardGrid);
        var stack = Stack();
        stack.Controls.Add(card);
        stack.Controls.Add(result);
        return stack;
    }

    private async void Next(object? sender, EventArgs args)
    {
        result.Text = "";
        if (currentStep == 0)
        {
            RefreshStatus();
            if (codexPath is null || tailnetUrl is null) { result.Text = L("Install and sign in to the required programs first.", "필수 프로그램의 설치와 로그인을 먼저 완료하세요."); ShowStep(0); return; }
            ShowStep(1);
            return;
        }
        if (currentStep == 1)
        {
            if (!ProjectSelectionIsValid()) { result.Text = L("Choose a project folder to use.", "사용할 프로젝트 폴더를 선택하세요."); return; }
            ShowStep(2);
            return;
        }
        await FinishSetupAsync();
    }

    private void RefreshStatus()
    {
        codexPath = FindCommand("codex.cmd") ?? FindCommand("codex.exe");
        tailscalePath = FindCommand("tailscale.exe") ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Tailscale", "tailscale.exe");
        if (!File.Exists(tailscalePath)) tailscalePath = null;
        codexStatus.Text = codexPath is null ? L("Not installed", "설치되지 않았습니다.") : L("✓  Installed and signed in", "✓  설치 및 로그인 확인됨");
        codexStatus.ForeColor = codexPath is null ? Danger : Success;
        codexAction.Visible = codexPath is null;
        tailnetUrl = ReadTailnetUrl();
        tailscaleStatus.Text = tailscalePath is null ? L("Not installed", "설치되지 않았습니다.") : tailnetUrl is null ? L("Sign-in required", "로그인이 필요합니다.") : L("✓  Connected", "✓  연결됨");
        tailscaleStatus.ForeColor = tailnetUrl is null ? Danger : Success;
        codexAction.Text = L("Install guide", "설치 안내");
        tailscaleAction.Text = tailscalePath is null ? L("Install guide", "설치 안내") : L("Sign in", "로그인");
        tailscaleAction.Visible = tailnetUrl is null;
        address.Text = tailnetUrl ?? L("The address appears after Tailscale connects.", "Tailscale 연결 후 주소가 표시됩니다.");
    }

    private void SelectProject(object? sender, EventArgs args)
    {
        using var picker = new FolderBrowserDialog
        {
            Description = L("Choose the project folder Codex can work in.", "Codex가 작업할 프로젝트 폴더를 선택하세요."),
            UseDescriptionForTitle = true,
            SelectedPath = Directory.Exists(projectPath.Text)
                ? projectPath.Text
                : Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)
        };
        if (picker.ShowDialog(this) == DialogResult.OK) projectPath.Text = picker.SelectedPath;
    }

    private void OpenCodex(object? sender, EventArgs args) => OpenUrl("https://developers.openai.com/codex/cli");

    private void OpenTailscale(object? sender, EventArgs args)
    {
        if (tailscalePath is null) OpenUrl("https://tailscale.com/download/windows");
        else Process.Start(new ProcessStartInfo(tailscalePath) { UseShellExecute = true });
    }

    private async void StartPairing(object? sender, EventArgs args)
    {
        result.Text = "";
        RefreshStatus();
        if (codexPath is null || tailscalePath is null || tailnetUrl is null)
        {
            result.ForeColor = Danger;
            result.Text = L("Connect Codex and Tailscale first.", "Codex\uC640 Tailscale\uC744 \uBA3C\uC800 \uC5F0\uACB0\uD558\uC138\uC694.");
            return;
        }
        if (!ProjectSelectionIsValid())
        {
            result.ForeColor = Danger;
            result.Text = L("Choose a project folder first.", "\uD504\uB85C\uC81D\uD2B8 \uD3F4\uB354\uB97C \uBA3C\uC800 \uC120\uD0DD\uD558\uC138\uC694.");
            return;
        }

        pairingAction.Enabled = false;
        nextButton.Enabled = false;
        try
        {
            await EnsureHostConfiguredAsync();
            var launcher = new DevicePairingLauncher(
                Path.Combine(AppContext.BaseDirectory, "node.exe"),
                Path.Combine(AppContext.BaseDirectory, "app", "backend", "src", "device-pairing-cli.js"),
                configurationPath);
            var pairing = await launcher.CreateAsync(tailnetUrl);
            activePairingCode = pairing.Code;
            pairingExpiresAt = pairing.ExpiresAt;
            RenderQr(pairing.QrPayload);
            pairingTimer.Start();
            UpdatePairingDisplay();
            result.ForeColor = Success;
            result.Text = L("Scan the QR or enter the code on your phone now.", "\uC9C0\uAE08 \uD734\uB300\uD3F0\uC5D0\uC11C QR\uC744 \uC2A4\uCE94\uD558\uAC70\uB098 \uCF54\uB4DC\uB97C \uC785\uB825\uD558\uC138\uC694.");
        }
        catch (ConfigurationMigrationException)
        {
            result.ForeColor = Danger;
            result.Text = L("The existing settings could not be updated safely.", "\uAE30\uC874 \uC124\uC815\uC744 \uC548\uC804\uD558\uAC8C \uC5C5\uB370\uC774\uD2B8\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
        }
        catch (TailscaleExposureException error)
        {
            result.ForeColor = Danger;
            result.Text = DescribeTailscaleExposureError(error);
        }
        catch
        {
            result.ForeColor = Danger;
            result.Text = L("A pairing code could not be created. Try again.", "\uD398\uC5B4\uB9C1 \uCF54\uB4DC\uB97C \uB9CC\uB4E4 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694.");
        }
        finally
        {
            pairingAction.Enabled = true;
            nextButton.Enabled = true;
        }
    }

    private void UpdatePairingDisplay()
    {
        pairingAction.Text = L(activePairingCode is null ? "Create pairing code" : "Create a new code", activePairingCode is null ? "\uD398\uC5B4\uB9C1 \uCF54\uB4DC \uB9CC\uB4E4\uAE30" : "\uC0C8 \uCF54\uB4DC \uB9CC\uB4E4\uAE30");
        if (activePairingCode is null || pairingExpiresAt is null)
        {
            pairingCode.Text = "---";
            pairingExpiry.Text = L("Valid for five minutes after creation.", "\uB9CC\uB4E0 \uD6C4 5\uBD84 \uB3D9\uC548 \uC0AC\uC6A9\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.");
            return;
        }

        var remaining = pairingExpiresAt.Value - DateTimeOffset.UtcNow;
        if (remaining <= TimeSpan.Zero)
        {
            pairingTimer.Stop();
            activePairingCode = null;
            pairingExpiresAt = null;
            RenderQr(null);
            pairingCode.Text = L("Expired", "\uB9CC\uB8CC\uB428");
            pairingExpiry.Text = L("Create a new code to connect another device.", "\uB2E4\uB978 \uAE30\uAE30\uB97C \uC5F0\uACB0\uD558\uB824\uBA74 \uC0C8 \uCF54\uB4DC\uB97C \uB9CC\uB4DC\uC138\uC694.");
            pairingAction.Text = L("Create pairing code", "\uD398\uC5B4\uB9C1 \uCF54\uB4DC \uB9CC\uB4E4\uAE30");
            return;
        }

        pairingCode.Text = activePairingCode;
        pairingExpiry.Text = L(
            $"Expires in {Math.Max(0, (int)remaining.TotalMinutes)}:{remaining.Seconds:00}",
            $"{Math.Max(0, (int)remaining.TotalMinutes)}:{remaining.Seconds:00} \uD6C4 \uB9CC\uB8CC");
    }

    private async Task EnsureHostConfiguredAsync()
    {
        if (hostPrepared) return;
        var candidate = await BuildConfigurationCandidateAsync();
        var backendPort = candidate["port"]?.GetValue<int>()
            ?? throw new ConfigurationMigrationException("configuration_invalid");
        var exposure = new TailscaleExposureGuard(tailscalePath!);
        exposure.EnsureSafe(backendPort);
        await SaveConfigurationAsync(candidate);
        RunTailscaleServe();
        exposure.EnsureSafe(backendPort);
        RegisterStartup();
        if (!await HostIsRunningAsync()) StartHost();
        hostPrepared = true;
    }

    private static async Task<bool> HostIsRunningAsync()
    {
        try
        {
            using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(2) };
            using var response = await client.GetAsync("http://127.0.0.1:43110/health");
            return response.IsSuccessStatusCode;
        }
        catch
        {
            return false;
        }
    }

    private async Task FinishSetupAsync()
    {
        result.Text = "";
        RefreshStatus();
        if (codexPath is null || tailscalePath is null || tailnetUrl is null) { ShowStep(0); result.Text = L("Install and sign in to the required programs first.", "필수 프로그램의 설치와 로그인을 먼저 완료하세요."); return; }
        if (!ProjectSelectionIsValid()) { ShowStep(1); result.Text = L("Choose a project folder to use.", "사용할 프로젝트 폴더를 선택하세요."); return; }
        try
        {
            await EnsureHostConfiguredAsync();
            result.ForeColor = Success;
            result.Text = L("Setup is complete. Opening Code Remote Manager.", "설정이 완료되었습니다. Code Remote Manager를 여는 중입니다.");
            nextButton.Enabled = false;
            OpenUrl("http://127.0.0.1:43111");
        }
        catch (ConfigurationMigrationException)
        {
            result.ForeColor = Danger;
            result.Text = L(
                "The existing settings could not be updated safely. No host changes were started.",
                "기존 설정을 안전하게 업데이트하지 못했습니다. 호스트 변경은 시작되지 않았습니다.");
        }
        catch (TailscaleExposureException error)
        {
            result.ForeColor = Danger;
            result.Text = DescribeTailscaleExposureError(error);
        }
        catch
        {
            result.ForeColor = Danger;
            result.Text = L("The host could not start. Close the existing Code Remote Manager host and try again.", "호스트를 시작하지 못했습니다. 기존 Code Remote Manager를 종료한 뒤 다시 시도하세요.");
        }
    }

    private bool ProjectSelectionIsValid() =>
        hasExistingConfiguration && string.IsNullOrWhiteSpace(projectPath.Text) ||
        Directory.Exists(projectPath.Text);

    private async Task<JsonObject> BuildConfigurationCandidateAsync()
    {
        var local = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
        var dataRoot = Path.Combine(local, "CodeRemoteManager");
        var home = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codex");
        var existingJson = File.Exists(configurationPath)
            ? await File.ReadAllTextAsync(configurationPath)
            : null;
        var selectedProject = string.IsNullOrWhiteSpace(projectPath.Text)
            ? null
            : Path.GetFullPath(projectPath.Text);
        return ConfigurationMigration.BuildCandidate(
            existingJson,
            new SetupConfigurationInput(codexPath!, tailnetUrl!, selectedProject),
            new SetupConfigurationDefaults(Path.Combine(dataRoot, "data"), home));
    }

    private async Task SaveConfigurationAsync(JsonObject candidate)
    {
        var validator = new BackendConfigurationValidator(
            Path.Combine(AppContext.BaseDirectory, "node.exe"),
            Path.Combine(AppContext.BaseDirectory, "app", "backend", "src", "config-validator-cli.js"));
        var store = new ConfigurationStore(validator.ValidateAsync);
        await store.SaveAsync(configurationPath, candidate);
    }

    private string DescribeTailscaleExposureError(TailscaleExposureException error) => error.Message switch
    {
        "tailscale_backend_exposed" => L(
            "Tailscale is exposing the private Backend. Remove that mapping before setup.",
            "Tailscale\uC774 \uBE44\uACF5\uAC1C Backend\uB97C \uB178\uCD9C\uD558\uACE0 \uC788\uC2B5\uB2C8\uB2E4. \uC124\uC815 \uC804\uC5D0 \uD574\uB2F9 \uB9E4\uD551\uC744 \uC81C\uAC70\uD558\uC138\uC694."),
        "tailscale_funnel_exposed" => L(
            "Tailscale Funnel is enabled. Disable public exposure before setup.",
            "Tailscale Funnel\uC774 \uCF1C\uC838 \uC788\uC2B5\uB2C8\uB2E4. \uC124\uC815 \uC804\uC5D0 \uACF5\uAC1C \uB178\uCD9C\uC744 \uB044\uC138\uC694."),
        _ => L(
            "Tailscale exposure could not be verified. No host changes were started.",
            "Tailscale \uB178\uCD9C \uC0C1\uD0DC\uB97C \uD655\uC778\uD560 \uC218 \uC5C6\uC5B4 \uD638\uC2A4\uD2B8 \uBCC0\uACBD\uC744 \uC2DC\uC791\uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.")
    };

    private string? ReadTailnetUrl()
    {
        if (tailscalePath is null) return null;
        try
        {
            var json = RunCapture(tailscalePath, "status --json");
            using var document = JsonDocument.Parse(json);
            if (document.RootElement.GetProperty("BackendState").GetString() != "Running") return null;
            var dns = document.RootElement.GetProperty("Self").GetProperty("DNSName").GetString()?.TrimEnd('.');
            return string.IsNullOrWhiteSpace(dns) ? null : $"https://{dns}";
        }
        catch { return null; }
    }

    private void RunTailscaleServe()
    {
        var process = Process.Start(new ProcessStartInfo(tailscalePath!, "serve --bg --yes http://127.0.0.1:43111") { UseShellExecute = false, CreateNoWindow = true });
        if (!process!.WaitForExit(15000)) { process.Kill(); throw new TimeoutException(); }
        if (process.ExitCode != 0) throw new InvalidOperationException();
    }

    private void RegisterStartup()
    {
        var script = Path.Combine(AppContext.BaseDirectory, "start-host.vbs");
        using var key = Registry.CurrentUser.CreateSubKey(@"Software\Microsoft\Windows\CurrentVersion\Run");
        key.DeleteValue("Code Remote Manager Host", false);
        key.SetValue("Code Remote Manager Host", $"wscript.exe \"{script}\"");
    }

    private static void StartHost() => Process.Start(new ProcessStartInfo("wscript.exe", $"\"{Path.Combine(AppContext.BaseDirectory, "start-host.vbs")}\"") { UseShellExecute = false, CreateNoWindow = true });

    private static string RunCapture(string file, string arguments)
    {
        var process = Process.Start(new ProcessStartInfo(file, arguments) { RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true })!;
        var output = process.StandardOutput.ReadToEnd();
        process.WaitForExit(10000);
        return output;
    }

    private static string? FindCommand(string command)
    {
        try { return RunCapture("where.exe", command).Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).FirstOrDefault(File.Exists); }
        catch { return null; }
    }

    private void RenderQr(string? value)
    {
        qr.Image?.Dispose();
        qr.Image = null;
        if (value is null) return;
        using var data = QRCodeGenerator.GenerateQrCode(value, QRCodeGenerator.ECCLevel.Q);
        using var code = new PngByteQRCode(data);
        using var stream = new MemoryStream(code.GetGraphic(8));
        using var decoded = new Bitmap(stream);
        qr.Image = new Bitmap(decoded);
    }

    private static TableLayoutPanel Stack() => new() { Dock = DockStyle.Top, AutoSize = true, ColumnCount = 1, Padding = new Padding(0) };
    private static Label Heading(string text) => new() { Text = text, AutoSize = true, Dock = DockStyle.Top, Font = new Font("Segoe UI Semibold", 14), ForeColor = Color.FromArgb(30, 30, 30) };
    private static Label Info(string text) => new() { Text = text, AutoSize = true, Dock = DockStyle.Top, ForeColor = Muted, Margin = new Padding(0, 8, 0, 0) };
    private static Label StatusLabel() => new() { AutoSize = true, ForeColor = Muted, Margin = new Padding(0, 5, 0, 0) };

    private static Control StatusCard(string name, Label status, Button action)
    {
        var card = new RoundedPanel { Dock = DockStyle.Top, Height = 104, Padding = new Padding(18), BackColor = Color.FromArgb(250, 250, 250), BorderColor = Color.FromArgb(220, 220, 220), Radius = 12, Margin = new Padding(0, 0, 0, 12) };
        var grid = new TableLayoutPanel { Dock = DockStyle.Fill, ColumnCount = 3, RowCount = 2, BackColor = Color.Transparent };
        grid.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 72));
        grid.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
        grid.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
        grid.Controls.Add(new IconBadge(name == "Codex CLI" ? IconKind.Terminal : IconKind.Tailscale) { Dock = DockStyle.Fill, Margin = new Padding(0, 0, 14, 0) }, 0, 0);
        grid.SetRowSpan(grid.GetControlFromPosition(0, 0)!, 2);
        grid.Controls.Add(new Label { Text = name, AutoSize = true, Font = new Font("Segoe UI Semibold", 13) }, 1, 0);
        grid.Controls.Add(status, 1, 1);
        grid.Controls.Add(action, 2, 0);
        grid.SetRowSpan(action, 2);
        card.Controls.Add(grid);
        return card;
    }

    private static Button SecondaryButton(string text) => new() { Text = text, AutoSize = true, MinimumSize = new Size(104, 38), FlatStyle = FlatStyle.Flat, BackColor = Color.White, ForeColor = Color.FromArgb(40, 40, 40), Margin = new Padding(0) };
    private static Button PrimaryButton(string text) { var button = new Button { Text = text, AutoSize = true, MinimumSize = new Size(124, 40), BackColor = Accent, ForeColor = Color.White, FlatStyle = FlatStyle.Flat, Margin = new Padding(0) }; button.FlatAppearance.BorderSize = 0; return button; }
    private static void OpenUrl(string url) => Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });
}

internal sealed class RoundedPanel : Panel
{
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    public int Radius { get; set; } = 10;
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    public Color BorderColor { get; set; } = Color.LightGray;

    protected override void OnPaint(PaintEventArgs e)
    {
        base.OnPaint(e);
        e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
        using var path = RoundedRectangle(ClientRectangle with { Width = ClientRectangle.Width - 1, Height = ClientRectangle.Height - 1 }, Radius);
        using var pen = new Pen(BorderColor);
        e.Graphics.DrawPath(pen, path);
    }

    private static GraphicsPath RoundedRectangle(Rectangle bounds, int radius)
    {
        var diameter = radius * 2;
        var path = new GraphicsPath();
        path.AddArc(bounds.Left, bounds.Top, diameter, diameter, 180, 90);
        path.AddArc(bounds.Right - diameter, bounds.Top, diameter, diameter, 270, 90);
        path.AddArc(bounds.Right - diameter, bounds.Bottom - diameter, diameter, diameter, 0, 90);
        path.AddArc(bounds.Left, bounds.Bottom - diameter, diameter, diameter, 90, 90);
        path.CloseFigure();
        return path;
    }
}

internal enum IconKind { Terminal, Tailscale, Folder }

internal sealed class IconBadge : Control
{
    private readonly IconKind kind;

    public IconBadge(IconKind kind)
    {
        this.kind = kind;
        SetStyle(ControlStyles.SupportsTransparentBackColor | ControlStyles.OptimizedDoubleBuffer | ControlStyles.UserPaint, true);
        BackColor = Color.Transparent;
        Size = new Size(62, 62);
        MinimumSize = new Size(58, 58);
    }

    protected override void OnPaint(PaintEventArgs e)
    {
        base.OnPaint(e);
        e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
        var size = Math.Min(58, Math.Min(Width - 2, Height - 2));
        var bounds = new Rectangle((Width - size) / 2, (Height - size) / 2, size, size);
        var background = kind switch
        {
            IconKind.Terminal => Color.FromArgb(235, 249, 238),
            IconKind.Tailscale => Color.FromArgb(239, 241, 255),
            _ => Color.FromArgb(255, 248, 224)
        };
        var border = kind switch
        {
            IconKind.Terminal => Color.FromArgb(183, 226, 190),
            IconKind.Tailscale => Color.FromArgb(196, 202, 239),
            _ => Color.FromArgb(241, 211, 124)
        };
        using var path = RoundedRectangle(bounds, 10);
        using var brush = new SolidBrush(background);
        using var pen = new Pen(border);
        e.Graphics.FillPath(brush, path);
        e.Graphics.DrawPath(pen, path);
        if (kind == IconKind.Terminal) DrawTerminal(e.Graphics, bounds);
        else if (kind == IconKind.Tailscale) DrawTailscale(e.Graphics, bounds);
        else DrawFolder(e.Graphics, bounds);
    }

    private static void DrawTerminal(Graphics graphics, Rectangle bounds)
    {
        using var pen = new Pen(Color.FromArgb(30, 30, 30), 3) { StartCap = LineCap.Round, EndCap = LineCap.Round };
        var x = bounds.Left + 17;
        var y = bounds.Top + 20;
        graphics.DrawLines(pen, [new Point(x, y), new Point(x + 7, y + 7), new Point(x, y + 14)]);
        graphics.DrawLine(pen, x + 13, y + 14, x + 25, y + 14);
    }

    private static void DrawTailscale(Graphics graphics, Rectangle bounds)
    {
        var colors = new[] { Color.FromArgb(160, 160, 170), Color.FromArgb(25, 25, 28) };
        for (var row = 0; row < 3; row++)
        for (var column = 0; column < 3; column++)
        {
            using var brush = new SolidBrush(colors[(row + column) % 2]);
            graphics.FillEllipse(brush, bounds.Left + 16 + column * 11, bounds.Top + 16 + row * 11, 7, 7);
        }
    }

    private static void DrawFolder(Graphics graphics, Rectangle bounds)
    {
        var body = new Rectangle(bounds.Left + 11, bounds.Top + 22, bounds.Width - 22, bounds.Height - 32);
        using var brush = new SolidBrush(Color.FromArgb(242, 184, 51));
        using var tab = new SolidBrush(Color.FromArgb(225, 158, 25));
        graphics.FillRectangle(tab, bounds.Left + 13, bounds.Top + 16, 18, 10);
        graphics.FillRectangle(brush, body);
    }

    private static GraphicsPath RoundedRectangle(Rectangle bounds, int radius)
    {
        var diameter = radius * 2;
        var path = new GraphicsPath();
        path.AddArc(bounds.Left, bounds.Top, diameter, diameter, 180, 90);
        path.AddArc(bounds.Right - diameter, bounds.Top, diameter, diameter, 270, 90);
        path.AddArc(bounds.Right - diameter, bounds.Bottom - diameter, diameter, diameter, 0, 90);
        path.AddArc(bounds.Left, bounds.Bottom - diameter, diameter, diameter, 90, 90);
        path.CloseFigure();
        return path;
    }
}

internal sealed class StepProgress : Control
{
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    public string[] Labels { get; set; } = ["Requirements", "Project folder", "Phone connection"];
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    public int CurrentStep { get; set; }

    public StepProgress()
    {
        DoubleBuffered = true;
        Font = new Font("Segoe UI Semibold", 10.5f);
        Height = 74;
    }

    protected override void OnPaint(PaintEventArgs e)
    {
        base.OnPaint(e);
        e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
        var centers = new[] { Width / 8, Width / 2, Width * 7 / 8 };
        var lineY = 23;
        using var inactivePen = new Pen(Color.FromArgb(215, 215, 215), 2);
        using var activePen = new Pen(Color.FromArgb(0, 103, 192), 2);
        e.Graphics.DrawLine(inactivePen, centers[0], lineY, centers[2], lineY);
        e.Graphics.DrawLine(activePen, centers[0], lineY, centers[CurrentStep], lineY);
        for (var i = 0; i < centers.Length; i++)
        {
            var completed = i < CurrentStep;
            var active = i == CurrentStep;
            var fill = active ? Color.FromArgb(0, 103, 192) : Color.White;
            var outline = completed ? Color.FromArgb(18, 135, 62) : active ? Color.FromArgb(0, 103, 192) : Color.FromArgb(185, 185, 185);
            using var brush = new SolidBrush(fill);
            using var pen = new Pen(outline, 2);
            e.Graphics.FillEllipse(brush, centers[i] - 17, lineY - 17, 34, 34);
            e.Graphics.DrawEllipse(pen, centers[i] - 17, lineY - 17, 34, 34);
            var marker = completed ? "✓" : (i + 1).ToString();
            var markerColor = active ? Color.White : outline;
            TextRenderer.DrawText(e.Graphics, marker, Font, new Rectangle(centers[i] - 16, lineY - 15, 32, 30), markerColor, TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter);
            TextRenderer.DrawText(e.Graphics, Labels[i], Font, new Rectangle(centers[i] - 100, 48, 200, 24), active ? Color.FromArgb(0, 103, 192) : Color.FromArgb(80, 80, 80), TextFormatFlags.HorizontalCenter);
        }
    }
}
