use crate::components::ComponentManager;
use crate::commands::{
    Command, CommandResult, ApplyStashCommand, CheckoutBranchCommand, MergeBranchCommand,
    PopStashCommand, RebaseBranchCommand, StageFilesCommand, UnstageFilesCommand,
    CommitCommand, PushCommand, CherryPickCommand, RevertCommand, RebaseContinueCommand, RebaseAbortCommand, ResetCommand,
    ListMergedBranchesCommand, PruneMergedBranchesCommand, RemotePruneCommand, ShowRemoteUrlCommand, SetRemoteSshCommand, LogBriefCommand, LogStatsCommand,
    PullCommand, PullRebaseCommand, OpenInEditorCommand, CheckMergeStatusCommand, AutoFetchCommand,
    StageHunkCommand, UnstageHunkCommand, StageLinesCommand, UnstageLinesCommand,
    RebaseTodoLoadCommand, RebaseTodoOpenCommand, RebaseTodoSaveCommand, RebaseSkipCommand,
    PrListCommand, PrCheckoutCommand, PrOpenCommand, CreateBranchCommand,
    CreateStashCommand, DeleteStashCommand,
};
use crate::commands::custom::CustomCommand;
use crate::errors::ApplicationError;
use super::{event_loop, AppState, Action, reducer};
use super::commit_template;
use crate::services::GitService;
use crate::git::parsers::diff;
use crate::async_result::AsyncResult;
use crossterm::event::EventStream;
use notify::RecommendedWatcher;
use ratatui::backend::CrosstermBackend;
use ratatui::Terminal;
use std::sync::Arc;
use tokio::sync::{mpsc, watch};

pub struct Application {
    pub(crate) state: AppState,
    pub(crate) component_manager: ComponentManager,
    pub(crate) git_service: Arc<GitService>,
    pub(crate) event_stream: EventStream,
    pub(crate) cancel_rx: watch::Receiver<bool>,
    pub(crate) async_tx: mpsc::UnboundedSender<AsyncResult>,
    pub(crate) async_rx: mpsc::UnboundedReceiver<AsyncResult>,
    pub(crate) _watcher: Option<RecommendedWatcher>, // Keep watcher alive
}

impl Application {
    pub fn new(
        state: AppState,
        component_manager: ComponentManager,
        git_service: Arc<GitService>,
        cancel_rx: watch::Receiver<bool>,
        async_tx: mpsc::UnboundedSender<AsyncResult>,
        async_rx: mpsc::UnboundedReceiver<AsyncResult>,
        watcher: Option<RecommendedWatcher>,
    ) -> Result<Self, ApplicationError> {
        let event_stream = EventStream::new();
        
        Ok(Self {
            state,
            component_manager,
            git_service,
            event_stream,
            cancel_rx,
            async_tx,
            async_rx,
            _watcher: watcher,
        })
    }
    
    pub async fn run(&mut self, terminal: &mut Terminal<CrosstermBackend<std::io::Stdout>>) -> Result<(), ApplicationError> {
        event_loop::run(self, terminal).await
    }
    
    /// Converts an action into a command for execution.
    /// 
    /// Returns `None` if the action doesn't require a command, or if the action
    /// cannot be executed in the current state (e.g., no files selected).
    pub(crate) fn action_to_command(&self, action: &Action) -> Option<Box<dyn Command>> {
        match action {
            Action::StageSelectedFile => {
                Application::create_stage_command(&self.state, true)
            }
            Action::UnstageSelectedFile => {
                Application::create_stage_command(&self.state, false)
            }
            Action::StageAllFiles => {
                let paths: Vec<String> = self
                    .state
                    .status_entries
                    .iter()
                    .filter(|e| e.unstaged || (!e.staged && !e.unstaged)) // unstaged or untracked
                    .map(|e| e.path.clone())
                    .collect();
                if paths.is_empty() {
                    None
                } else {
                    Some(Box::new(StageFilesCommand { paths }))
                }
            }
            Action::UnstageAllFiles => {
                let paths: Vec<String> = self
                    .state
                    .status_entries
                    .iter()
                    .filter(|e| e.staged)
                    .map(|e| e.path.clone())
                    .collect();
                if paths.is_empty() {
                    None
                } else {
                    Some(Box::new(UnstageFilesCommand { paths }))
                }
            }
            Action::CommitSubmit => {
                Application::create_commit_command(&self.state, self.state.amend_mode)
            }
            Action::AmendCommit => {
                // StartAmend is handled in event loop, not here
                None
            }
            Action::Push => {
                Some(Box::new(PushCommand {
                    force_with_lease: !self.state.push_ff_only_enforce, // if guard on, avoid force-with-lease; if off, allow
                }))
            }
            Action::CherryPickSelected => {
                let hash = self
                    .state
                    .commits
                    .get(self.state.commit_selected)
                    .map(|c| c.hash.clone());
                hash.map(|h| Box::new(CherryPickCommand { hash: h }) as Box<dyn Command>)
            }
            Action::RevertSelected => {
                let hash = self
                    .state
                    .commits
                    .get(self.state.commit_selected)
                    .map(|c| c.hash.clone());
                hash.map(|h| Box::new(RevertCommand { hash: h }) as Box<dyn Command>)
            }
            Action::RebaseContinue => Some(Box::new(RebaseContinueCommand)),
            Action::RebaseAbort => Some(Box::new(RebaseAbortCommand)),
            Action::RebaseSkip => Some(Box::new(RebaseSkipCommand)),
            Action::SaveRebaseTodo => Some(Box::new(RebaseTodoSaveCommand)),
            Action::ShowRebaseTodo => Some(Box::new(RebaseTodoLoadCommand)),
            Action::OpenRebaseTodoInEditor => Some(Box::new(RebaseTodoOpenCommand)),
            Action::CheckAutoFetch => Some(Box::new(AutoFetchCommand {
                remote: self.state.auto_fetch_remote.clone(),
            })),
            Action::ShowPrHelper => Some(Box::new(PrListCommand {
                remote: self.state.auto_fetch_remote.clone(),
            })),
            Action::CheckoutPr(pr) => Some(Box::new(PrCheckoutCommand {
                remote: self.state.auto_fetch_remote.clone(),
                pr: pr.clone(),
            })),
            Action::OpenPrInBrowser(pr) => Some(Box::new(PrOpenCommand {
                remote: self.state.auto_fetch_remote.clone(),
                pr: pr.clone(),
            })),
            Action::ResetSoft => Some(Box::new(ResetCommand {
                mode: "--soft",
                target: "HEAD~1".to_string(),
            })),
            Action::ResetMixed => Some(Box::new(ResetCommand {
                mode: "--mixed",
                target: "HEAD~1".to_string(),
            })),
            Action::ResetHard => Some(Box::new(ResetCommand {
                mode: "--hard",
                target: "HEAD~1".to_string(),
            })),
            Action::ListMergedBranches => Some(Box::new(ListMergedBranchesCommand {
                base: self.state.merge_base_branch.clone(),
            })),
            Action::PruneMergedBranches => Some(Box::new(PruneMergedBranchesCommand {
                base: self.state.merge_base_branch.clone(),
            })),
            Action::RemotePrune(remote) => Some(Box::new(RemotePruneCommand {
                remote: remote.clone(),
            })),
            Action::ShowRemoteUrl(remote) => Some(Box::new(ShowRemoteUrlCommand {
                remote: remote.clone(),
            })),
            Action::SetRemoteSsh(remote) => Some(Box::new(SetRemoteSshCommand {
                remote: remote.clone(),
            })),
            Action::ShowLogBrief => Some(Box::new(LogBriefCommand { limit: 5 })),
            Action::ShowLogStats => Some(Box::new(LogStatsCommand { limit: 5 })),
            Action::Pull => Some(Box::new(PullCommand {
                ff_only: self.state.pull_ff_only,
                timeout_secs: self.state.pull_timeout_secs,
            })),
            Action::PullRebase => Some(Box::new(PullRebaseCommand {
                autostash: self.state.pull_autostash,
                timeout_secs: self.state.rebase_timeout_secs,
            })),
            Action::OpenInEditor => {
                if self.state.status_selected_path.is_empty() {
                    None
                } else {
                    Some(Box::new(OpenInEditorCommand {
                        path: self.state.status_selected_path.clone(),
                        line: None,
                    }))
                }
            }
            Action::OpenConflictInEditor(path) => {
                Some(Box::new(OpenInEditorCommand {
                    path: path.clone(),
                    line: None,
                }))
            }
            Action::CheckMergeStatus => Some(Box::new(CheckMergeStatusCommand {
                base: self.state.merge_base_branch.clone(),
            })),
            Action::ShowMergeBasePicker
            | Action::HideMergeBasePicker
            | Action::MergeBaseUp
            | Action::MergeBaseDown
            | Action::TogglePushFFOnly => None,
            Action::ApplyMergeBase(name) => Some(Box::new(CheckMergeStatusCommand { base: name.clone() })),
            Action::StageCurrentHunk => {
                let parsed = diff::parse_diff(&self.state.last_diff).ok()?;
                // Defensive: use first() instead of get(0) for clarity, handle empty files
                let file = parsed.files.first()?;
                let idx = self.state.hunk_selected.min(file.hunks.len().saturating_sub(1));
                Some(Box::new(StageHunkCommand {
                    hunk_index: idx,
                    parsed,
                    path: self.state.last_diff_path.clone(),
                    raw_diff: self.state.last_diff.clone(),
                }))
            }
            Action::UnstageCurrentHunk => {
                let parsed = diff::parse_diff(&self.state.last_diff).ok()?;
                // Defensive: use first() instead of get(0) for clarity, handle empty files
                let file = parsed.files.first()?;
                let idx = self.state.hunk_selected.min(file.hunks.len().saturating_sub(1));
                Some(Box::new(UnstageHunkCommand {
                    hunk_index: idx,
                    parsed,
                    path: self.state.last_diff_path.clone(),
                    raw_diff: self.state.last_diff.clone(),
                }))
            }
            Action::StageSelectedLines => {
                if self.state.selected_lines.is_empty() {
                    None
                } else {
                    Some(Box::new(StageLinesCommand {
                        raw_diff: self.state.last_diff.clone(),
                        path: self.state.last_diff_path.clone(),
                        selected_lines: self.state.selected_lines.clone(),
                    }))
                }
            }
            Action::UnstageSelectedLines => {
                if self.state.selected_lines.is_empty() {
                    None
                } else {
                    Some(Box::new(UnstageLinesCommand {
                        raw_diff: self.state.last_diff.clone(),
                        path: self.state.last_diff_path.clone(),
                        selected_lines: self.state.selected_lines.clone(),
                    }))
                }
            }
            Action::CheckoutBranch(branch_name) => {
                Some(Box::new(CheckoutBranchCommand { branch_name: branch_name.clone() }))
            }
            Action::MergeBranch(branch_name) => {
                Some(Box::new(MergeBranchCommand { branch_name: branch_name.clone() }))
            }
            Action::RebaseBranch(branch_name) => {
                Some(Box::new(RebaseBranchCommand { branch_name: branch_name.clone() }))
            }
            Action::ApplyStash(stash_name) => {
                Some(Box::new(ApplyStashCommand { name: stash_name.clone() }))
            }
            Action::PopStash(stash_name) => {
                Some(Box::new(PopStashCommand { name: stash_name.clone() }))
            }
            Action::DeleteStash(stash_name) => {
                Some(Box::new(DeleteStashCommand { name: stash_name.clone() }))
            }
            Action::BranchSubmit => {
                Some(Box::new(CreateBranchCommand {
                    branch_name: self.state.branch_name_input.clone(),
                }))
            }
            Action::StashSubmit => {
                Some(Box::new(CreateStashCommand {
                    message: self.state.stash_message_input.clone(),
                }))
            }
            Action::ExecuteCustomCommand(name) => {
                if let Some(command) = self.state.custom_commands.get(name.as_str()) {
                    Some(Box::new(CustomCommand {
                        name: name.clone(),
                        command: command.clone(),
                    }))
                } else {
                    None
                }
            }
            _ => None,
        }
    }
    
    pub(crate) async fn apply_command_result(&mut self, result: CommandResult) -> Result<(), ApplicationError> {
        match result {
            CommandResult::StateUpdate(new_state) => {
                self.state = new_state;
                // If refreshing flag is set, trigger status refresh
                if self.state.refreshing {
                    // StatusPane will handle the refresh in its update method
                    // For now, we'll let the component handle it
                }
            }
            CommandResult::AsyncTask(task) => {
                self.state = reducer(self.state.clone(), Action::AddAsyncTask(task));
            }
            CommandResult::NoOp => {}
        }
        Ok(())
    }
    
    pub(crate) async fn handle_async_result(&mut self, result: AsyncResult) -> Result<(), ApplicationError> {
        match result {
            AsyncResult::TaskComplete { id, success, message, should_refresh } => {
                if success {
                    self.state = reducer(self.state.clone(), Action::CompleteAsyncTask(id));
                    self.state = reducer(self.state.clone(), Action::SetFeedback(Some(message)));
                } else {
                    self.state = reducer(self.state.clone(), Action::FailAsyncTask(id, message.clone()));
                    self.state = reducer(self.state.clone(), Action::SetStatusError(Some(message)));
                }
                if should_refresh {
                    // Trigger refresh
                    if let Some(component) = self.component_manager.active_mut() {
                        let _ = component.update(Action::RefreshStatus, &mut self.state);
                    }
                }
            }
            AsyncResult::FileChanged => {
                // Auto-refresh on file changes
                if let Some(component) = self.component_manager.active_mut() {
                    let _ = component.update(Action::RefreshStatus, &mut self.state);
                }
            }
            AsyncResult::CommitDetailLoaded(detail) => {
                // Update commit detail with loaded content
                self.state = reducer(self.state.clone(), Action::SetCommitDetail(Some(detail)));
            }
            AsyncResult::CommitDetailError(error) => {
                // Show error and clear loading state
                self.state = reducer(self.state.clone(), Action::SetStatusError(Some(error)));
                self.state = reducer(self.state.clone(), Action::SetCommitDetail(None));
            }
        }
        // Clean up old completed tasks
        self.state = reducer(self.state.clone(), Action::CleanupAsyncTasks);
        Ok(())
    }
    
    /// Creates a stage or unstage command for selected files.
    /// 
    /// **File Selection Priority:**
    /// 1. Multi-selected files (`selected_files`) if any
    /// 2. Currently selected file (`status_selected_path`) if available
    /// 3. None if no files are selected
    /// 
    /// Returns `None` if no files are available for the operation.
    fn create_stage_command(state: &AppState, stage: bool) -> Option<Box<dyn Command>> {
        let paths: Vec<String> = if state.selected_files.is_empty() {
            // No multi-selection - use currently selected file
            if !state.status_selected_path.is_empty() {
                vec![state.status_selected_path.clone()]
            } else {
                vec![] // No file selected
            }
        } else {
            // Multi-selection active - use selected files
            state.selected_files.iter().cloned().collect()
        };
        
        if paths.is_empty() {
            None
        } else if stage {
            Some(Box::new(StageFilesCommand { paths }))
        } else {
            Some(Box::new(UnstageFilesCommand { paths }))
        }
    }
    
    /// Creates a commit command with template substitution applied.
    /// 
    /// **Template Processing:**
    /// - If a commit template exists, applies template substitution
    /// - Replaces template variables (e.g., `{message}`, `{branch}`, `{date}`)
    /// - Returns `None` if the final message is empty after trimming
    /// 
    /// **Parameters:**
    /// - `state`: Current application state
    /// - `amend`: Whether this is an amend operation
    fn create_commit_command(state: &AppState, amend: bool) -> Option<Box<dyn Command>> {
        let user_message = state.commit_input.trim().to_string();
        if user_message.is_empty() {
            return None;
        }
        
        // Apply template substitution if template exists
        let final_message = if let Some(ref template) = state.commit_template {
            commit_template::substitute_template(template, state, &user_message)
        } else {
            user_message
        };
        
        // For amend, use author info if provided
        let author_name = if amend && !state.amend_author_name.is_empty() {
            Some(state.amend_author_name.clone())
        } else {
            None
        };
        let author_email = if amend && !state.amend_author_email.is_empty() {
            Some(state.amend_author_email.clone())
        } else {
            None
        };
        
        Some(Box::new(CommitCommand {
            message: final_message,
            amend,
            author_name,
            author_email,
        }))
    }
}