pub mod async_job;

use ratatui::DefaultTerminal;

use crate::app_context::AppContext;
use crate::component::{Action, Component, FilerComponent, PreviewMove, TreeComponent, prompt};
use crate::event::{EventHandler, InputEvent};
use crate::fs::VFile;
use crate::fs::async_job::FileJob;
use crate::state::{PasteBuffer, PasteMode, Phase};
use crate::store::RootStore;
use crate::ui;
use anyhow::{Context, Result};
use crossterm::execute;
use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen};
use std::io::stdout;
use std::time::{Duration, Instant};

/// プレビューの n/p 連打時、再生成を最大この間隔に抑える（連打のフリーズ緩和）。
/// 単発操作はこの間隔を超えているため即時再生成され、入力停止後はアイドルで確実に最終位置を再生成する。
/// 「入力停止後の追従」は `EventHandler::next_event` のタイムアウト（現状 100ms）でアイドルを
/// 検知することに依存する。その値を変えると停止後の追従遅延も変わる点に注意。
const PREVIEW_REBUILD_THROTTLE: Duration = Duration::from_millis(120);

/// 先頭の `~` をホームディレクトリへ展開する。`~` 単体・`~/...` のみ対応し、
/// `~user` 形式や展開不能時は入力をそのまま `PathBuf` として返す。
fn expand_tilde(path: &str) -> std::path::PathBuf {
    if let Some(rest) = path.strip_prefix('~')
        && (rest.is_empty() || rest.starts_with('/'))
        && let Some(home) = dirs::home_dir()
    {
        return home.join(rest.trim_start_matches('/'));
    }
    std::path::PathBuf::from(path)
}

/// Search 系アクション（インクリメンタル検索・次/前・Esc 復元）の適用先。
/// ツリーパネルが開いていればツリー、そうでなければ Filer。Filer の Search
/// プロンプトはサイドパネルが閉じているときしか開かないため、検索中の対象は一意に定まる。
enum SearchTarget<'a> {
    Tree(&'a mut TreeComponent),
    Filer(&'a mut FilerComponent),
}

impl SearchTarget<'_> {
    fn select_matching(&mut self, query: &str) {
        match self {
            SearchTarget::Tree(tree) => tree.select_matching(query),
            SearchTarget::Filer(filer) => filer.select_matching_file(query),
        }
    }

    fn select_next(&mut self, query: &str) {
        match self {
            SearchTarget::Tree(tree) => tree.select_next_matching(query),
            SearchTarget::Filer(filer) => filer.select_next_matching_file(query),
        }
    }

    fn select_prev(&mut self, query: &str) {
        match self {
            SearchTarget::Tree(tree) => tree.select_prev_matching(query),
            SearchTarget::Filer(filer) => filer.select_prev_matching_file(query),
        }
    }

    /// Search の Esc 復元で、控えておいたカーソル位置へ戻す。
    fn restore_index(&mut self, index: usize) {
        match self {
            SearchTarget::Tree(tree) => tree.select_index(Some(index)),
            SearchTarget::Filer(filer) => filer.select_file_table(Some(index)),
        }
    }
}

pub struct App {
    ctx: AppContext,
    store: RootStore,
    event_handler: EventHandler,
    skip_history_add: bool,
    /// 現在ファイル変更を監視しているディレクトリ。アクティブ Context のカレント
    /// ディレクトリが変わった（ナビゲーション or Context 切替）ときに張り替える。
    watching_dir_path: String,
    /// プレビューのカーソルが動いたが、まだパネルを再生成していない。
    preview_dirty: bool,
    /// 直近でプレビューパネルを再生成した時刻（スロットル判定用）。
    last_preview_build: Instant,
}

impl App {
    pub fn new(picker: ratatui_image::picker::Picker) -> Result<Self> {
        Ok(Self {
            ctx: AppContext::new(picker),
            store: RootStore::new()?,
            event_handler: EventHandler::default(),
            skip_history_add: false,
            watching_dir_path: String::new(),
            preview_dirty: false,
            last_preview_build: Instant::now(),
        })
    }

    pub fn init(&mut self) -> Result<()> {
        if let Err(e) = self.store.init() {
            tracing::warn!("Failed to initialize store: {}", e);
        }
        let startup_dir = self.resolve_startup_directory();
        self.ctx.init(startup_dir)?;
        // 最初の Context の戻る/進む履歴に永続履歴を引き継ぎ、従来どおり過去の
        // 訪問先へ `<`/`>` で戻れるようにする。
        self.ctx
            .active_history_mut()
            .reset_with(self.store.history.entries_snapshot());
        Ok(())
    }

    fn resolve_startup_directory(&self) -> Option<std::path::PathBuf> {
        use crate::store::StartupDirectory;
        match self.store.settings.startup_directory() {
            StartupDirectory::CurrentDirectory => None,
            StartupDirectory::HomeDirectory => dirs::home_dir(),
            StartupDirectory::LastDirectory => self
                .store
                .history
                .last_entry()
                .filter(|p| std::path::Path::new(p).is_dir())
                .map(std::path::PathBuf::from)
                .or_else(dirs::home_dir),
            // 指定パスを `~` 展開して解決。存在するディレクトリならそれを使い、
            // 存在しなければ None（= カレントディレクトリ）にフォールバックする。
            StartupDirectory::SpecificDirectory(path) => {
                let resolved = expand_tilde(path);
                resolved.is_dir().then_some(resolved)
            }
        }
    }

    /// alternate screen を離脱してクロージャを実行し、完了後に復帰する。
    fn run_in_shell_mode<F>(
        terminal: &mut DefaultTerminal,
        event_handler: &EventHandler,
        f: F,
    ) -> Result<()>
    where
        F: FnOnce() -> Result<()>,
    {
        event_handler.pause();

        crossterm::terminal::disable_raw_mode()?;
        execute!(stdout(), LeaveAlternateScreen)?;

        let result = f();

        let r1 = execute!(stdout(), EnterAlternateScreen);
        let r2 = crossterm::terminal::enable_raw_mode();
        let r3 = terminal.clear();

        event_handler.resume();

        r1.and(r2).and(r3)?;

        result
    }

    fn default_shell() -> String {
        std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string())
    }

    fn launch_external_shell(
        ctx: &AppContext,
        terminal: &mut DefaultTerminal,
        event_handler: &EventHandler,
    ) -> Result<()> {
        let shell = Self::default_shell();
        let dir = ctx.active_filer().current_dir_path().to_string();

        Self::run_in_shell_mode(terminal, event_handler, || {
            std::process::Command::new(&shell)
                .current_dir(&dir)
                .status()
                .with_context(|| format!("シェルの起動に失敗しました: {shell}"))?;
            Ok(())
        })
    }

    fn execute_shell_command(
        command: String,
        dir: String,
        terminal: &mut DefaultTerminal,
        event_handler: &EventHandler,
    ) -> Result<()> {
        let shell = Self::default_shell();

        Self::run_in_shell_mode(terminal, event_handler, || {
            std::process::Command::new(&shell)
                .arg("-c")
                .arg(&command)
                .current_dir(&dir)
                .status()
                .with_context(|| format!("コマンドの実行に失敗しました: {command}"))?;

            eprintln!("\nPress Enter to continue...");
            let _ = std::io::stdin().read_line(&mut String::new());
            Ok(())
        })
    }

    fn set_error(&mut self, message: String) {
        self.ctx.prompt.set_error(message);
    }

    /// 現在の Search 系アクションの適用先を返す。ツリーパネルが開いていればツリー、
    /// そうでなければ Filer。
    fn search_target(&mut self) -> SearchTarget<'_> {
        // active_filer_mut() は ctx 全体を可変借用するため、side_panel の借用と両立しない。
        // 先に Tree かどうかを判定して借用を閉じてから、対象を可変借用し直す。
        // （単一 match や早期 return で書くと side_panel の借用が末尾まで延び E0499 になる。）
        let is_tree = matches!(self.ctx.side_panel, Some(crate::state::SidePanel::Tree(_)));
        if is_tree {
            if let Some(crate::state::SidePanel::Tree(tree)) = self.ctx.side_panel.as_mut() {
                return SearchTarget::Tree(tree);
            }
            unreachable!("is_tree で Tree を確認済み");
        }
        SearchTarget::Filer(self.ctx.active_filer_mut())
    }

    /// プレビューの n/p 移動で生じた再生成を、スロットル/アイドルに応じて 1 回だけ反映する。
    /// 連打中はカーソル移動のみ蓄積し、ここで間引いて再生成することでフリーズを防ぐ。
    /// `idle` が true（入力が一定時間届かず `next_event` がタイムアウトした）のときは
    /// スロットルを無視して必ず再生成し、連打停止後に最終位置へ確実に追従させる。
    fn flush_preview_rebuild(&mut self, idle: bool) {
        if !self.preview_dirty {
            return;
        }
        if !idle && self.last_preview_build.elapsed() < PREVIEW_REBUILD_THROTTLE {
            return;
        }
        self.preview_dirty = false;
        self.last_preview_build = Instant::now();
        // プレビューパネルが開いているときだけ差し替える（閉じられていれば何もしない）。
        if self
            .ctx
            .side_panel
            .as_ref()
            .is_some_and(|panel| panel.is_preview())
        {
            self.ctx.side_panel = self.ctx.active_filer().build_preview_panel();
        }
    }

    /// Action を処理する
    fn handle_action(&mut self, action: Action, terminal: &mut DefaultTerminal) -> Result<()> {
        match action {
            Action::None => {}
            Action::Quit => self.ctx.quit(),
            Action::LaunchShell => {
                if let Err(e) =
                    Self::launch_external_shell(&self.ctx, terminal, &self.event_handler)
                {
                    self.set_error(format!("{e}"));
                }
            }
            Action::CloseSidePanel => {
                self.ctx.side_panel = None;
                // パネルを閉じたら未反映のプレビュー移動は破棄する（再オープン誤動作の防止）。
                self.preview_dirty = false;
            }
            Action::NavigateTo(path) => {
                self.ctx.side_panel = None;
                self.ctx.active_filer_mut().jump_to(&path)?;
            }
            Action::RemoveBookmark(path) => {
                self.store.bookmark.remove(&path)?;
            }
            Action::AddBookmark(path) => {
                self.store.bookmark.add(&path)?;
            }
            Action::ExecutePrompt(input) => {
                prompt::execute_prompt_action(&mut self.ctx, &mut self.store, *input)?;
            }
            Action::ExecuteCommand(command, dir) => {
                if let Err(e) =
                    Self::execute_shell_command(command, dir, terminal, &self.event_handler)
                {
                    self.set_error(format!("{e}"));
                }
            }
            Action::CancelPrompt => {
                if let Some(idx) = self.ctx.prompt.cancel() {
                    // Search の Esc 復元。ツリー検索ならツリー、そうでなければ Filer のカーソルへ戻す。
                    self.search_target().restore_index(idx);
                }
            }
            Action::SearchUpdate(value) => {
                self.search_target().select_matching(&value);
            }
            Action::FilterUpdate(value) => {
                self.ctx.active_filer_mut().set_name_filter(&value);
            }
            Action::SearchNext(value) => {
                self.search_target().select_next(&value);
            }
            Action::SearchPrev(value) => {
                self.search_target().select_prev(&value);
            }
            Action::SetPromptMode(mode) => {
                self.ctx.prompt.set_mode(*mode);
            }
            Action::ShowSidePanel(panel) => {
                if self.ctx.side_panel.is_none() {
                    self.ctx.side_panel = Some(*panel);
                }
            }
            Action::PreviewNext => {
                // カーソルを動かすのみ。実際のパネル再生成は run ループで
                // スロットル/アイドルに応じてまとめて行う（連打時のフリーズ緩和）。
                if self
                    .ctx
                    .active_filer_mut()
                    .move_preview_cursor(PreviewMove::Next)
                {
                    self.preview_dirty = true;
                }
            }
            Action::PreviewPrev => {
                if self
                    .ctx
                    .active_filer_mut()
                    .move_preview_cursor(PreviewMove::Prev)
                {
                    self.preview_dirty = true;
                }
            }
            Action::OpenFile(path) => {
                open::that(&path).with_context(|| format!("ファイルを開けませんでした: {path}"))?;
            }
            Action::OpenInFileManager(path) => {
                open::that(&path)
                    .with_context(|| format!("ファイルマネージャで開けませんでした: {path}"))?;
            }
            Action::Yank(paths) => {
                if let Err(e) = crate::os::clipboard::write_paths(&paths) {
                    tracing::warn!("yank failed: {e:#}");
                    self.set_error(format!("{e:#}"));
                }
            }
            Action::SetPermissions(path, mode) => {
                if let Err(e) = VFile::new(path).set_permissions(mode) {
                    // context チェーンまで表示する（権限不足・読み取り専用 FS 等の原因提示）。
                    self.set_error(format!("{e:#}"));
                } else {
                    // 一覧の rwx 表示を更新するため再読み込みする。
                    self.ctx.active_filer_mut().refresh_files();
                }
            }
            Action::CopyToPasteBuffer(paths) => {
                if !paths.is_empty() {
                    self.ctx.paste_buffer = Some(PasteBuffer {
                        paths,
                        mode: PasteMode::Copy,
                    });
                }
            }
            Action::CutToPasteBuffer(paths) => {
                if !paths.is_empty() {
                    self.ctx.paste_buffer = Some(PasteBuffer {
                        paths,
                        mode: PasteMode::Cut,
                    });
                }
            }
            Action::Paste => {
                // バッファから (mode, files) を取り出す。paths を clone せず、借用を
                // ここで閉じてから可変借用（start_file_job）へ進む。
                let prepared = self.ctx.paste_buffer.as_ref().map(|buffer| {
                    let files: Vec<VFile> = buffer
                        .paths
                        .iter()
                        .map(|p| VFile::new(p.as_str()))
                        .collect();
                    (buffer.mode, files)
                });
                if let Some((mode, files)) = prepared {
                    let dest = std::path::PathBuf::from(self.ctx.active_filer().current_dir_path());
                    let (job, phase) = match mode {
                        PasteMode::Copy => (FileJob::Copy { files, dest }, Phase::Scanning),
                        PasteMode::Cut => (FileJob::Move { files, dest }, Phase::Moving),
                    };
                    let started = prompt::start_file_job(&mut self.ctx, job, phase);
                    // Cut は paste で消費するためクリアするが、起動できたときだけ
                    // （別ジョブ実行中で起動しなかった場合は対象を失わない）。
                    if started && mode == PasteMode::Cut {
                        self.ctx.paste_buffer = None;
                    }
                }
            }
            Action::ShowBookmark => {
                if self.ctx.side_panel.is_none() {
                    let paths = self.store.bookmark.get_paths().cloned().collect();
                    self.ctx.side_panel = Some(crate::state::SidePanel::Bookmark(
                        crate::component::BookmarkComponent::new(paths),
                    ));
                }
            }
            Action::ShowSettings => {
                if self.ctx.side_panel.is_none() {
                    let startup_dir = self.store.settings.startup_directory().clone();
                    let current_dir = self.ctx.active_filer().current_dir_path().to_string();
                    self.ctx.side_panel = Some(crate::state::SidePanel::Settings(
                        crate::component::SettingsComponent::new(&startup_dir, &current_dir),
                    ));
                }
            }
            Action::SaveSettings(startup_dir) => {
                // 永続化のみ。パネルは閉じない（保存後は Settings の項目メニューへ戻る）。
                // 閉じる操作は CloseSidePanel に一本化している。
                self.store.settings.set_startup_directory(*startup_dir)?;
            }
            Action::NavigateBack => {
                if let Some(path) = self.ctx.active_history_mut().back().map(String::from) {
                    self.ctx.active_filer_mut().change_to(&path);
                    self.skip_history_add = true;
                }
            }
            Action::NavigateForward => {
                if let Some(path) = self.ctx.active_history_mut().forward().map(String::from) {
                    self.ctx.active_filer_mut().change_to(&path);
                    self.skip_history_add = true;
                }
            }
            Action::NextContext => {
                self.ctx.next_context();
                self.after_switch_active();
            }
            Action::PrevContext => {
                self.ctx.prev_context();
                self.after_switch_active();
            }
            Action::NewContext => {
                if let Err(e) = self.ctx.new_context() {
                    self.set_error(format!("{e:#}"));
                } else {
                    // 新規 Context は init でロード済みなので refresh は不要。dir を監視し直す
                    // （失敗時はアクティブ Context が変わらないので監視はそのまま）。
                    self.resync_active_dir_watch();
                }
            }
            Action::CloseContext => {
                self.ctx.close_context();
                self.after_switch_active();
            }
        }
        Ok(())
    }

    /// Context 切替/クローズ後の共通後処理。アクティブ Context を（非アクティブ中の
    /// ディスク変更を反映するため）再読込し、監視ディレクトリを張り替える。
    /// 新規 Context（`new_context`）は `init` でフレッシュにロード済みのため別扱い。
    fn after_switch_active(&mut self) {
        self.ctx.active_filer_mut().refresh_files();
        self.resync_active_dir_watch();
    }

    /// Context 切替/生成/クローズ後に、アクティブ Context のディレクトリを監視し直す。
    /// 切替はナビゲーションではないため、履歴（DirHistory / history.json）には記録しない。
    fn resync_active_dir_watch(&mut self) {
        let dir = self.ctx.active_filer().current_dir_path().to_string();
        if dir != self.watching_dir_path {
            if let Err(e) = self.event_handler.watch_directory(&dir) {
                tracing::warn!("Failed to watch directory: {e}");
            }
            self.watching_dir_path = dir;
        }
    }

    pub fn run(&mut self, terminal: &mut DefaultTerminal) -> Result<()> {
        self.watching_dir_path = self.ctx.active_filer().current_dir_path().to_string();
        // 起動直後の初期ディレクトリにも監視を張る。ループ内の watch_directory は
        // ナビゲートで current_dir が変わったときしか呼ばれないため、ここで一度設定する。
        // 監視は自動更新のための付加機能なので、失敗してもアプリ起動は止めず警告に留める。
        if let Err(e) = self.event_handler.watch_directory(&self.watching_dir_path) {
            tracing::warn!("Failed to watch startup directory: {e}");
        }

        while self.ctx.running {
            // UI を描画
            terminal.draw(|frame| ui::render_main_view(frame, &mut self.ctx, &self.store))?;

            // イベントを取得して処理
            let event = self.event_handler.next_event()?;
            let idle = matches!(event, InputEvent::None);
            match event {
                InputEvent::Key(key) => {
                    let action = if self.ctx.prompt.is_active() {
                        self.ctx.prompt.handle_event(key)?
                    } else if let Some(panel) = self.ctx.side_panel.as_mut() {
                        panel.handle_event(key)?
                    } else {
                        self.ctx.active_filer_mut().handle_event(key)?
                    };
                    if let Err(e) = self.handle_action(action, terminal) {
                        self.set_error(format!("{e}"));
                    }
                }
                InputEvent::FileChange => {
                    if !self.ctx.active_filer().is_loading() {
                        self.ctx.active_filer_mut().refresh_files();
                    }
                }
                InputEvent::None => {}
            }

            // プレビューの n/p 移動を反映する。入力が落ち着いた（アイドル）か、前回再生成から
            // 一定時間経過したときに 1 回だけ再生成し、連打時のフルリビルド連発を防ぐ。
            self.flush_preview_rebuild(idle);

            // コンポーネントのtick処理（非同期結果の受信等）
            self.ctx.tick();

            // 非同期ロードのエラーを検知して表示
            if let Some(error) = self.ctx.active_filer_mut().take_error() {
                self.set_error(error);
            }

            // Filer のフォーカス状態を更新（Focused View は同時に1つ）
            let focused = self.ctx.side_panel.is_none() && !self.ctx.prompt.is_active();
            self.ctx.active_filer_mut().set_focused(focused);

            // カレントディレクトリの監視と履歴保存
            let dir_changed = self.ctx.active_filer().current_dir_path() != self.watching_dir_path;
            if dir_changed {
                // active_history_mut()/store.history は ctx・store を可変借用するため、
                // カレントパスは先に所有文字列として取り出してから扱う。
                let current = self.ctx.active_filer().current_dir_path().to_string();
                // 監視失敗は致命ではないため警告に留める（起動時と同方針）。
                if let Err(e) = self.event_handler.watch_directory(&current) {
                    tracing::warn!("Failed to watch directory: {e}");
                }
                if self.skip_history_add {
                    self.skip_history_add = false;
                } else {
                    // 戻る/進むは Context ごと（DirHistory）、永続ログは global（HistoryStore）。
                    self.ctx.active_history_mut().push(&current);
                    if let Err(e) = self.store.history.add(&current) {
                        tracing::warn!("Failed to save history: {e}");
                    }
                }
                self.watching_dir_path = current;
            }
        }
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn expand_tilde_leaves_absolute_path_unchanged() {
        assert_eq!(
            expand_tilde("/tmp/foo"),
            std::path::PathBuf::from("/tmp/foo")
        );
    }

    #[test]
    fn expand_tilde_expands_bare_tilde_to_home() {
        if let Some(home) = dirs::home_dir() {
            assert_eq!(expand_tilde("~"), home);
        }
    }

    #[test]
    fn expand_tilde_expands_tilde_slash_prefix() {
        if let Some(home) = dirs::home_dir() {
            assert_eq!(expand_tilde("~/projects"), home.join("projects"));
        }
    }

    #[test]
    fn expand_tilde_leaves_tilde_user_form_unexpanded() {
        // ~user 形式は非対応。そのまま返す（存在しなければ起動時にフォールバック）。
        assert_eq!(expand_tilde("~bob/x"), std::path::PathBuf::from("~bob/x"));
    }
}
