// M4+M2+M5: AI provider wired, SQLite profile DB, live sidebar, filter chips.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using DansoriEQ.App.Ai;
using DansoriEQ.App.Apo;
using DansoriEQ.App.Controls;
using DansoriEQ.App.Input;
using DansoriEQ.App.Profiles;
using DansoriEQ.App.Switching;
using DansoriEQ.Core.Ai;
using DansoriEQ.Core.Apo;
using DansoriEQ.Core.Eq;
using DansoriEQ.Core.Presets;
using DansoriEQ.Core.Profiles;
using SWC = System.Windows.Controls;
using WpfUi = Wpf.Ui.Controls;

namespace DansoriEQ.App;

public partial class MainWindow : WpfUi.FluentWindow
{
    private readonly IApoWriter _apo = ApoWriterFactory.Create();
    private readonly System.Windows.Threading.DispatcherTimer _applyTimer;
    private readonly SqliteProfileDbService _db = new(AppPaths.DbPath);

    private EffectsConfig _effects = new();
    private readonly PresetLibrary _library = new(AppPaths.PresetsDir);
    private bool _bypassed = true;
    private HotkeyManager? _hotkeys;
    private ProfileSwitcher? _switcher;
    private int _presetIndex = -1;
    private readonly Stack<EqState> _undo = new();
    private EqState _baseState = new();
    private ProfileRef? _selectedProfile;
    private double _volumeDb;   // software volume attenuation (dB) applied via APO preamp

    private readonly List<HistoryItem> _history = new();

    public MainWindow()
    {
        InitializeComponent();

        var demo = new EqState { PreampDb = -6.0 };
        demo.Filters.Add(new Filter { Type = FilterType.LowShelf,  Fc = 90,    GainDb =  4.0, Q = 0.7 });
        demo.Filters.Add(new Filter { Type = FilterType.Peaking,   Fc = 180,   GainDb = -2.5, Q = 1.0 });
        demo.Filters.Add(new Filter { Type = FilterType.Peaking,   Fc = 3000,  GainDb =  3.0, Q = 1.2 });
        demo.Filters.Add(new Filter { Type = FilterType.Peaking,   Fc = 6500,  GainDb = -2.0, Q = 3.0 });
        demo.Filters.Add(new Filter { Type = FilterType.HighShelf, Fc = 10000, GainDb = -1.5, Q = 0.7 });
        Graph.State = demo;
        _baseState = Graph.State.Clone();

        _applyTimer = new System.Windows.Threading.DispatcherTimer
            { Interval = TimeSpan.FromMilliseconds(120) };
        _applyTimer.Tick += (_, __) => { _applyTimer.Stop(); RecomputePreampAndApply(); };
        Graph.EqChanged += (_, __) => { _applyTimer.Stop(); _applyTimer.Start(); };

        ApplyEqToApo();
        if (!_apo.IsLive)
            Title += "   —   APO 미설치(미리보기 모드)";

        _switcher = new ProfileSwitcher(_library, ApplyPreset);
        if (AppSettings.Load().AutoSwitchEnabled) _switcher.Start();

        // Software volume: when the output device has no hardware volume (e.g. line-out DAC),
        // VolumePanel raises this with a dB attenuation that we bake into the APO preamp.
        VolPanel.SoftVolumeChanged += db =>
        {
            _volumeDb = db;
            ApplyEqToApo();
        };

        // Initial sidebar + DB info
        RefreshSidebar();
        RefreshDbStats();
        RefreshFilterChips();

        // Background mascot float — driven here so its rhythm (and talk reaction) stays
        // in sync with the prompt-dock avatar (MascotAvatar). Starts calm idle.
        Loaded += (_, __) =>
        {
            SetBgMascotTalking(false);
            Live2DCharacter.SetState("idle");
            Live2DCharacter.ShowText("Ready");
        };
    }

    // ── Background mascot (big 이소리) motion, synced with the dock avatar ──
    // Idle: slow gentle float. Talking (AI responding): livelier, faster bob.
    // Same up-first direction & similar period as MascotAvatar so they move together.
    private void SetBgMascotTalking(bool talking)
    {
        var anim = new DoubleAnimation
        {
            From = 0,
            To = talking ? -22 : -12,
            Duration = TimeSpan.FromMilliseconds(talking ? 1300 : 2200),
            AutoReverse = true,
            RepeatBehavior = RepeatBehavior.Forever,
            EasingFunction = new SineEase { EasingMode = EasingMode.EaseInOut }
        };
        BgSoriFloat.BeginAnimation(TranslateTransform.YProperty, anim);
    }

    // ── Hotkeys ──
    protected override void OnSourceInitialized(EventArgs e)
    {
        base.OnSourceInitialized(e);
        _hotkeys = new HotkeyManager(this);
        _hotkeys.Register(0x0002 | 0x0001, 0x42, () => ActiveToggle.IsChecked = !(ActiveToggle.IsChecked ?? true));
        _hotkeys.Register(0x0002 | 0x0001, 0xDD, () => CyclePreset(1));
        _hotkeys.Register(0x0002 | 0x0001, 0xDB, () => CyclePreset(-1));
    }

    // ── Sidebar — search + profile selection (M2) ──
    private void SidebarSearch_TextChanged(object sender, TextChangedEventArgs e)
        => RefreshSidebar(SidebarSearch.Text);

    private void RefreshSidebar(string? query = null)
    {
        var q = string.IsNullOrWhiteSpace(query) ? null : query.Trim();
        IReadOnlyList<ProfileRef> results;
        try { results = _db.Search(q, null); }
        catch { results = Array.Empty<ProfileRef>(); }

        IemList.SelectionChanged -= IemList_SelectionChanged;
        HeadphoneList.SelectionChanged -= HeadphoneList_SelectionChanged;

        IemList.ItemsSource = results.Where(r => r.Type == "iem").ToList();
        HeadphoneList.ItemsSource = results.Where(r => r.Type == "headphone").ToList();

        // Restore selection highlight after refresh
        if (_selectedProfile != null)
        {
            if (_selectedProfile.Type == "iem")
                IemList.SelectedItem = ((List<ProfileRef>)IemList.ItemsSource)
                    .FirstOrDefault(p => p.Id == _selectedProfile.Id);
            else
                HeadphoneList.SelectedItem = ((List<ProfileRef>)HeadphoneList.ItemsSource)
                    .FirstOrDefault(p => p.Id == _selectedProfile.Id);
        }

        IemList.SelectionChanged += IemList_SelectionChanged;
        HeadphoneList.SelectionChanged += HeadphoneList_SelectionChanged;
    }

    private void IemList_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        if (IemList.SelectedItem is ProfileRef p)
        {
            HeadphoneList.SelectionChanged -= HeadphoneList_SelectionChanged;
            HeadphoneList.SelectedItem = null;
            HeadphoneList.SelectionChanged += HeadphoneList_SelectionChanged;
            OnProfileSelected(p);
        }
    }

    private void HeadphoneList_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        if (HeadphoneList.SelectedItem is ProfileRef p)
        {
            IemList.SelectionChanged -= IemList_SelectionChanged;
            IemList.SelectedItem = null;
            IemList.SelectionChanged += IemList_SelectionChanged;
            OnProfileSelected(p);
        }
    }

    private void EmptyHintClose_Click(object sender, RoutedEventArgs e)
        => EmptyHint.Visibility = Visibility.Collapsed;

    private void OnProfileSelected(ProfileRef profile)
    {
        _selectedProfile = profile;
        if (EmptyHint != null) EmptyHint.Visibility = Visibility.Collapsed;
        HeadphoneLabel.Text = profile.Name;
        HeadphoneTypeBadge.Text = profile.Type == "iem" ? "IEM" : "HEADPHONE";
        HeadphoneTypeBorder.Background = profile.Type == "iem"
            ? new SolidColorBrush(Color.FromArgb(0x24, 0x38, 0xE0, 0xC4))
            : new SolidColorBrush(Color.FromArgb(0x24, 0x4C, 0xC2, 0xFF));
        HeadphoneTypeBadge.Foreground = profile.Type == "iem"
            ? new SolidColorBrush(Color.FromRgb(0x38, 0xE0, 0xC4))
            : new SolidColorBrush(Color.FromRgb(0x4C, 0xC2, 0xFF));

        EqState? eq = null;
        try { eq = _db.LoadEq(profile.Id); } catch { /* DB error — keep current */ }

        if (eq != null)
        {
            PushUndo();
            _baseState = eq.Clone();
            Graph.State = eq.Clone();
            RecomputePreampAndApply();
        }

        HeadphoneSourceBadge.Visibility = Visibility.Visible;
        HeadphoneSourceText.Text = "AutoEQ · DB";
    }

    private void RefreshDbStats()
    {
        try
        {
            var info = _db.GetInfo();
            var updated = info.LastUpdatedUtc?.ToString("yyyy-MM-dd") ?? "없음";
            DbStatsText.Text = $"{info.TotalProfiles:N0} profiles · 최신: {updated}";
        }
        catch { DbStatsText.Text = "DB를 업데이트해 프로파일을 불러오세요"; }
    }

    // ── EQ / APO ──
    private void Active_Changed(object sender, RoutedEventArgs e)
    {
        if (Graph is null) return;
        _bypassed = ActiveToggle.IsChecked != true;
        ApplyEqToApo();
    }

    private void ApplyEqToApo()
    {
        if (Graph is null) return;
        _apo.Apply(_bypassed
            ? $"Preamp: {_volumeDb.ToString("0.0", System.Globalization.CultureInfo.InvariantCulture)} dB\n"
            : ApoRenderer.Render(Graph.State, _effects, "Dansori EQ", _volumeDb));
        RefreshPreampLabel();
        RefreshFilterChips();
    }

    private void RefreshPreampLabel()
    {
        if (PreampLabel is null || Graph is null) return;
        var db = Graph.State.PreampDb;
        PreampLabel.Text = $"Preamp {(db >= 0 ? "+" : "")}{db:0.0} dB";
    }

    private void RefreshFilterChips()
    {
        if (FilterChipsPanel is null || Graph is null) return;
        FilterChipsPanel.Children.Clear();

        var filters = Graph.State.Filters;
        int enabled = filters.Count(f => f.Enabled);
        if (FilterCountLabel != null)
            FilterCountLabel.Text = $"ACTIVE FILTERS · {enabled} / {EqGraphControl.MaxBands} bands";

        var card2 = new SolidColorBrush(Color.FromRgb(0x2D, 0x2D, 0x33));
        var stroke = new SolidColorBrush(Color.FromRgb(0x3A, 0x3A, 0x42));
        var textDim = new SolidColorBrush(Color.FromRgb(0xA8, 0xAB, 0xB4));
        var green = new SolidColorBrush(Color.FromRgb(0x4F, 0xD0, 0x8A));
        var amber = new SolidColorBrush(Color.FromRgb(0xFF, 0xB4, 0x54));

        for (int i = 0; i < filters.Count; i++)
        {
            var f = filters[i];
            if (!f.Enabled) continue;
            int capturedIndex = i;

            var typeLabel = f.Type.ShortLabel();
            var fc = f.Fc >= 1000 ? $"{f.Fc / 1000:0.#} kHz" : $"{f.Fc:0} Hz";

            // Third param descriptor depends on the filter type.
            var third = f.Type.ThirdParam() switch
            {
                FilterParam.Q     => $"Q{f.Q:0.#}",
                FilterParam.Slope => f.Type is FilterType.LowShelfSlope or FilterType.HighShelfSlope
                                        ? $"{f.Slope:0}dB/oct" : $"S{f.Slope:0.0}",
                FilterParam.Order => $"{f.Order}차",
                _ => ""
            };

            var tb = new SWC.TextBlock { Foreground = textDim };
            tb.Inlines.Add(new Run($"{typeLabel}  {fc} · {third}  "));
            if (f.Type.UsesGain())
            {
                var gainText = f.GainDb >= 0 ? $"+{f.GainDb:0.#} dB" : $"{f.GainDb:0.#} dB";
                tb.Inlines.Add(new Run(gainText) { Foreground = f.GainDb >= 0 ? green : amber });
            }

            var chip = new Border
            {
                Background = card2,
                BorderBrush = stroke,
                BorderThickness = new Thickness(1),
                CornerRadius = new CornerRadius(9),
                Padding = new Thickness(11, 7, 11, 7),
                Margin = new Thickness(0, 0, 8, 8),
                Cursor = System.Windows.Input.Cursors.Hand,
                ToolTip = "더블클릭: 세부 편집",
                Child = tb
            };
            chip.MouseLeftButtonDown += (_, ev) =>
            {
                if (ev.ClickCount == 2) OpenFilterEdit(capturedIndex);
            };
            FilterChipsPanel.Children.Add(chip);
        }
    }

    private void OpenFilterEdit(int index)
    {
        if (index < 0 || index >= Graph.State.Filters.Count) return;
        var win = new FilterEditWindow(Graph.State, index) { Owner = this };
        if (win.ShowDialog() == true)
        {
            _undo.Push(Graph.State.Clone());
            Graph.State = win.ResultState;
            RecomputePreampAndApply();
            RefreshFilterChips();
            Graph.InvalidateVisual();
        }
    }

    private void RecomputePreampAndApply()
    {
        Graph.State.PreampDb = PreampCalculator.ComputeSafePreamp(Graph.State);
        Graph.InvalidateVisual();
        ApplyEqToApo();
    }

    // ── More dropdown ──
    private void BtnMore_Click(object sender, RoutedEventArgs e)
    {
        var btn = (System.Windows.Controls.Button)sender;
        btn.ContextMenu!.PlacementTarget = btn;
        btn.ContextMenu.Placement = System.Windows.Controls.Primitives.PlacementMode.Bottom;
        btn.ContextMenu.IsOpen = true;
    }

    // ── Band controls ──
    private void BtnAddBand_Click(object sender, RoutedEventArgs e) { PushUndo(); Graph.AddBand(); }
    private void BtnClearBands_Click(object sender, RoutedEventArgs e) { PushUndo(); Graph.ClearBands(); }
    private void PushUndo() => _undo.Push(Graph.State.Clone());

    private void BtnUndo_Click(object sender, RoutedEventArgs e)
    {
        if (_undo.Count == 0) { AiResponse.Text = "되돌릴 항목이 없습니다."; return; }
        Graph.State = _undo.Pop();
        ApplyEqToApo();
    }

    private void BtnReset_Click(object sender, RoutedEventArgs e)
    {
        PushUndo();
        Graph.State = _baseState.Clone();
        ApplyEqToApo();
    }

    // ── Preset cycle ──
    private void CyclePreset(int dir)
    {
        var names = _library.List();
        if (names.Count == 0) { AiResponse.Text = "저장된 프리셋이 없습니다."; return; }
        _presetIndex = ((_presetIndex + dir) % names.Count + names.Count) % names.Count;
        try { ApplyPreset(_library.Load(names[_presetIndex])); AiResponse.Text = $"프리셋: {names[_presetIndex]}"; }
        catch { /* preset missing */ }
    }

    private void CharacterSelect_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        if (Live2DCharacter is null || CharacterSelect?.SelectedItem is not ComboBoxItem item) return;
        var characterId = item.Tag?.ToString() ?? "leesori";
        Live2DCharacter.SetCharacter(characterId);
        Live2DCharacter.SetState("idle");
        Live2DCharacter.ShowText($"{item.Content} ready");
    }
    // ── AI prompt → provider → EQ (M4) ──
    private async void BtnSend_Click(object sender, RoutedEventArgs e) => await SendPromptAsync();
    private async void Prompt_KeyDown(object sender, KeyEventArgs e)
    {
        if (e.Key == Key.Enter) { e.Handled = true; await SendPromptAsync(); }
    }

    private async System.Threading.Tasks.Task SendPromptAsync()
    {
        var text = PromptBox.Text?.Trim();
        if (string.IsNullOrEmpty(text)) return;

        var provider = AiProviderFactory.Create(new Security.DpapiSecretStore());
        if (provider is null)
        {
            AiResponse.Text = "AI API 키가 없습니다. ⚙ 설정 → 🧩 AI 관리에서 키를 입력하거나 로컬(Ollama) 모델을 선택하세요.";
            return;
        }

        // M4: use selected profile name dynamically
        var headphone = _selectedProfile?.Name ?? "선택된 이어폰 없음";
        var hpType    = _selectedProfile?.Type ?? "iem";

        AiResponse.Text = "AI가 EQ를 생성 중...";
        Live2DCharacter.SetState("thinking");
        Live2DCharacter.ShowText("AI is tuning EQ");
        Mascot.StartTalking();
        SetBgMascotTalking(true);
        AvatarRings.Start();
        ThinkingBars.Visibility = Visibility.Visible;
        ThinkingBars.Start();
        try
        {
            var currentEq = ApoRenderer.Render(Graph.State);
            var delta = await provider.RequestEqAsync(headphone, hpType, currentEq, text);
            PushUndo();
            Graph.State = EqDeltaApplier.Apply(Graph.State, delta);
            if (delta.Effects is { } fx)
            {
                if (fx.Crossfeed.HasValue)     _effects.CrossfeedLevel  = fx.Crossfeed.Value;
                if (fx.BassBoostDb.HasValue)   _effects.BassBoostDb     = fx.BassBoostDb.Value;
                if (fx.PreampLeftDb.HasValue)  _effects.PreampLeftDb    = fx.PreampLeftDb.Value;
                if (fx.PreampRightDb.HasValue) _effects.PreampRightDb   = fx.PreampRightDb.Value;
            }
            ApplyEqToApo();

            _history.Add(new HistoryItem { Role = "user", Text = text });
            _history.Add(new HistoryItem { Role = "ai",   Text = delta.Explanation });
            AiResponse.Text = string.IsNullOrWhiteSpace(delta.Explanation)
                ? "적용됨 ✓"
                : delta.Explanation + "   · 적용됨 ✓";
            PromptBox.Clear();
            SuccessToast.Show("적용 완료!", "EQ가 적용되었습니다.");
            Live2DCharacter.SetState("success");
            Live2DCharacter.ShowText("EQ applied");
        }
        catch (Exception ex)
        {
            AiResponse.Text = "오류: " + ex.Message;
            Live2DCharacter.SetState("error");
            Live2DCharacter.ShowText(ex.Message);
        }
        finally
        {
            Mascot.StopTalking();
            SetBgMascotTalking(false);
            AvatarRings.Stop();
            ThinkingBars.Stop();
            ThinkingBars.Visibility = Visibility.Collapsed;
        }
    }

    // ── Window buttons ──
    private void BtnDbUpdate_Click(object sender, RoutedEventArgs e)
    {
        new DbManagerWindow(_db) { Owner = this }.ShowDialog();
        RefreshDbStats();
        RefreshSidebar(SidebarSearch.Text);
    }

    private void BtnSettings_Click(object sender, RoutedEventArgs e)
        => new SettingsWindow(new Security.DpapiSecretStore()) { Owner = this }.ShowDialog();

    private void BtnCommand_Click(object sender, RoutedEventArgs e)
        => new CommandWindow { Owner = this }.ShowDialog();

    private void BtnEffects_Click(object sender, RoutedEventArgs e)
        => new EffectsWindow(_effects, ApplyEqToApo) { Owner = this }.ShowDialog();

    private void BtnPresets_Click(object sender, RoutedEventArgs e)
        => new PresetLibraryWindow(_library, CaptureCurrentPreset, ApplyPreset) { Owner = this }.ShowDialog();

    // ── Export / Import ──
    private EqPreset CaptureCurrentPreset()
    {
        var target = new PresetTarget
        {
            Name = _selectedProfile?.Name ?? "알 수 없는 기기",
            Brand = _selectedProfile?.Name?.Split(' ').FirstOrDefault() ?? "",
            Type = _selectedProfile?.Type ?? "iem",
            Source = "AutoEQ", TargetCurve = "Harman IE 2019"
        };
        return PresetSerializer.FromEqState(Graph.State, target, _history, DateTimeOffset.Now, "라이브러리 저장", _effects);
    }

    private void BtnExport_Click(object sender, RoutedEventArgs e)
    {
        var sfd = new Microsoft.Win32.SaveFileDialog
            { Filter = "Dansori EQ (*.tweq)|*.tweq", FileName = "preset.tweq" };
        if (sfd.ShowDialog() != true) return;
        var target = new PresetTarget
        {
            Name = _selectedProfile?.Name ?? "알 수 없는 기기",
            Brand = _selectedProfile?.Name?.Split(' ').FirstOrDefault() ?? "",
            Type = _selectedProfile?.Type ?? "iem",
            Source = "AutoEQ", TargetCurve = "Harman IE 2019"
        };
        var preset = PresetSerializer.FromEqState(Graph.State, target, _history, DateTimeOffset.Now, "내보낸 EQ", _effects);
        File.WriteAllText(sfd.FileName, PresetSerializer.Serialize(preset));
    }

    private void BtnImport_Click(object sender, RoutedEventArgs e)
    {
        var ofd = new Microsoft.Win32.OpenFileDialog { Filter = "Dansori EQ (*.tweq)|*.tweq" };
        if (ofd.ShowDialog() != true) return;
        try
        {
            var preset = PresetSerializer.Deserialize(File.ReadAllText(ofd.FileName));
            bool matched = _db.FindMatch(preset.Target.Name, preset.Target.Brand, preset.Target.Type) != null;
            var info = new PresetInfoWindow(preset, matched, _db) { Owner = this };
            if (info.ShowDialog() == true)
            {
                Graph.State = PresetSerializer.ToEqState(preset);
                _effects = PresetSerializer.ToEffects(preset);
                ApplyEqToApo();
            }
        }
        catch (Exception ex)
        {
            System.Windows.MessageBox.Show(this, "가져오기 실패: " + ex.Message, "오류",
                System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Warning);
        }
    }

    private void ApplyPreset(EqPreset p)
    {
        PushUndo();
        Graph.State = PresetSerializer.ToEqState(p);
        _effects = PresetSerializer.ToEffects(p);
        _baseState = Graph.State.Clone();
        ApplyEqToApo();
    }

    // ── Help drawer ──
    private void BtnHelp_Click(object sender, RoutedEventArgs e) => ToggleHelp(HelpPanel.ActualWidth < 10);
    private void HelpClose_Click(object sender, RoutedEventArgs e) => ToggleHelp(false);

    private void ToggleHelp(bool open)
    {
        var anim = new System.Windows.Media.Animation.DoubleAnimation(
            open ? 380 : 0, new Duration(TimeSpan.FromMilliseconds(250)))
        {
            EasingFunction = new System.Windows.Media.Animation.QuadraticEase
                { EasingMode = System.Windows.Media.Animation.EasingMode.EaseOut }
        };
        HelpPanel.BeginAnimation(WidthProperty, anim);
    }

    private void HelpUrl_Click(object sender, RoutedEventArgs e)
    {
        if (sender is FrameworkElement fe && fe.Tag is string url) OpenUrl(url);
    }

    private static void OpenUrl(string url)
    {
        try { System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(url) { UseShellExecute = true }); }
        catch { /* ignore */ }
    }
}




