use std::time::Duration;

use anyhow::Result;
use colored::Colorize;
use tokio::time::sleep;

use crate::api::{
    PlaneApiClient, PlaneState, PlaneWorkItem, ScheduledDate, StateGroup, WorkItemIdentifier,
    WorkItemPriority,
};
use crate::config::resolve_plane_session;
use crate::git_context::{BranchInference, infer_work_item_from_branch};

/// Live-tails a work item, polling for state changes at a configurable interval.
/// Displays the current state on first fetch, then prints field-level diffs as
/// changes are detected. Runs until interrupted with Ctrl+C.
/// Usage: `plane watch NRO-123` or `plane watch` (infers from branch)
#[derive(clap::Args)]
pub struct WatchWorkItemArgs {
    /// Work item identifier (e.g. "NRO-123"). Inferred from git branch if omitted.
    identifier: Option<String>,
    /// Polling interval in seconds
    #[arg(long, default_value = "5")]
    interval: u64,
}

/// Execution logic for the `plane watch` subcommand
impl WatchWorkItemArgs {
    /// Resolves the work item, prints its current state, then enters a polling loop
    /// that detects and prints field changes until the process is interrupted
    pub async fn execute(self) -> Result<()> {
        let work_item_identifier: WorkItemIdentifier = match self.identifier {
            Option::Some(ref identifier_text) => identifier_text.parse()?,
            Option::None => match infer_work_item_from_branch() {
                BranchInference::Resolved(inferred_identifier) => inferred_identifier,
                BranchInference::NotInGitRepo => {
                    anyhow::bail!(
                        "no identifier provided and not in a git repo — \
                         usage: plane watch NRO-123"
                    );
                }
                BranchInference::NoIdentifierInBranch(branch_name) => {
                    anyhow::bail!(
                        "no identifier provided and branch '{}' does not contain a work item ID",
                        branch_name
                    );
                }
            },
        };

        let plane_session = resolve_plane_session()?;
        let api_client = PlaneApiClient::try_new(plane_session)?;

        let fetched_work_item = api_client
            .find_work_item_by_identifier(&work_item_identifier)
            .await?;

        let project_states = api_client
            .list_states(fetched_work_item.project_id())
            .await?;

        let initial_source = SnapshotSource {
            work_item: &fetched_work_item,
            project_states: &project_states,
        };
        let mut current_snapshot = WorkItemSnapshot::capture(initial_source);

        let change_renderer = WatchRenderer {
            work_item_identifier: &work_item_identifier,
        };
        change_renderer.print_initial_state(&current_snapshot);

        println!(
            "Watching {} — polling every {}s, Ctrl+C to stop\n",
            work_item_identifier, self.interval
        );

        let poll_interval = Duration::from_secs(self.interval);
        let project_client = api_client.project_operations(fetched_work_item.project_id());
        let item_client = project_client.work_item_operations(fetched_work_item.id());

        loop {
            sleep(poll_interval).await;

            let refreshed_item = match item_client.fetch_detail().await {
                Ok(item) => item,
                Err(fetch_error) => {
                    eprintln!("  poll error: {}", fetch_error);
                    continue;
                }
            };

            let refresh_source = SnapshotSource {
                work_item: &refreshed_item,
                project_states: &project_states,
            };
            let new_snapshot = WorkItemSnapshot::capture(refresh_source);

            let comparison = SnapshotComparison {
                previous: &current_snapshot,
                current: &new_snapshot,
            };
            change_renderer.print_changes(comparison);
            current_snapshot = new_snapshot;
        }
    }
}

/// References needed to create a displayable snapshot from API data
struct SnapshotSource<'a> {
    work_item: &'a PlaneWorkItem,
    project_states: &'a [PlaneState],
}

/// Resolved workflow state with the human-readable label and canonical group
/// for colorized terminal output and change detection
#[derive(PartialEq, Eq)]
struct ResolvedWorkflowState {
    label: String,
    group: StateGroup,
}

/// Colorized terminal rendering for resolved workflow state labels
impl ResolvedWorkflowState {
    /// Returns the label with ANSI coloring derived from the canonical workflow group
    fn colorized_label(&self) -> String {
        match self.group {
            StateGroup::Backlog => self.label.dimmed().to_string(),
            StateGroup::Unstarted => self.label.white().to_string(),
            StateGroup::Started => self.label.yellow().to_string(),
            StateGroup::Completed => self.label.green().to_string(),
            StateGroup::Cancelled => self.label.red().to_string(),
        }
    }
}

/// Snapshot of a work item's displayable fields captured at a single point in time.
/// Two snapshots are compared field-by-field to detect and report changes.
struct WorkItemSnapshot {
    workflow_state: ResolvedWorkflowState,
    priority: WorkItemPriority,
    title_text: String,
    assignee_count: usize,
    due_date_text: String,
}

/// Snapshot construction from fetched API data with state UUID resolution
impl WorkItemSnapshot {
    /// Extracts the displayable fields from a fetched work item, resolving the
    /// state UUID to a named label using the project's state definitions
    fn capture(source: SnapshotSource<'_>) -> Self {
        let mut resolved_label = source.work_item.workflow_state_id().to_string();
        let mut resolved_group = StateGroup::Backlog;
        for candidate_state in source.project_states {
            if candidate_state.id() == source.work_item.workflow_state_id() {
                resolved_label = candidate_state.display_name().to_owned();
                resolved_group = candidate_state.state_group();
            }
        }

        let due_date_text = match source.work_item.target_date() {
            ScheduledDate::Scheduled(date_text) => date_text.to_owned(),
            ScheduledDate::Unscheduled => String::from("unscheduled"),
        };

        Self {
            workflow_state: ResolvedWorkflowState {
                label: resolved_label,
                group: resolved_group,
            },
            priority: source.work_item.priority(),
            title_text: source.work_item.title().as_str().to_owned(),
            assignee_count: source.work_item.assignee_ids().len(),
            due_date_text,
        }
    }
}

/// References to two consecutive snapshots for field-level change comparison
struct SnapshotComparison<'a> {
    previous: &'a WorkItemSnapshot,
    current: &'a WorkItemSnapshot,
}

/// Handles all terminal output for the watch polling loop, including the initial
/// state display and timestamped change reports
struct WatchRenderer<'a> {
    work_item_identifier: &'a WorkItemIdentifier,
}

/// Initial state rendering and change detection output for the polling loop
impl<'a> WatchRenderer<'a> {
    /// Prints the full current state of the work item as the initial display
    /// before entering the polling loop
    fn print_initial_state(&self, snapshot: &WorkItemSnapshot) {
        println!(
            "{} {}",
            self.work_item_identifier.to_string().bold(),
            snapshot.title_text.bold()
        );
        println!(
            "  State:     {}",
            snapshot.workflow_state.colorized_label()
        );
        println!("  Priority:  {}", format_priority(snapshot.priority));
        println!("  Assignees: {}", snapshot.assignee_count);
        println!("  Due:       {}", snapshot.due_date_text);
        println!();
    }

    /// Compares two snapshots and prints timestamped field-level changes.
    /// Produces no output if all tracked fields are unchanged between polls.
    fn print_changes(&self, comparison: SnapshotComparison<'_>) {
        let previous = comparison.previous;
        let current = comparison.current;

        if previous.title_text == current.title_text
            && previous.workflow_state == current.workflow_state
            && previous.priority == current.priority
            && previous.assignee_count == current.assignee_count
            && previous.due_date_text == current.due_date_text
        {
            return;
        }

        let timestamp = chrono::Local::now().format("%H:%M:%S");
        println!("[{}] {} changed:", timestamp, self.work_item_identifier);

        if previous.title_text != current.title_text {
            println!(
                "  Title:     {} -> {}",
                previous.title_text,
                current.title_text.bold()
            );
        }
        if previous.workflow_state != current.workflow_state {
            println!(
                "  State:     {} -> {}",
                previous.workflow_state.label,
                current.workflow_state.colorized_label()
            );
        }
        if previous.priority != current.priority {
            println!(
                "  Priority:  {} -> {}",
                format_priority(previous.priority),
                format_priority(current.priority)
            );
        }
        if previous.assignee_count != current.assignee_count {
            println!(
                "  Assignees: {} -> {}",
                previous.assignee_count, current.assignee_count
            );
        }
        if previous.due_date_text != current.due_date_text {
            println!(
                "  Due:       {} -> {}",
                previous.due_date_text, current.due_date_text
            );
        }
        println!();
    }
}

fn format_priority(work_item_priority: WorkItemPriority) -> String {
    match work_item_priority {
        WorkItemPriority::Urgent => "Urgent".red().bold().to_string(),
        WorkItemPriority::High => "High".red().to_string(),
        WorkItemPriority::Medium => "Medium".yellow().to_string(),
        WorkItemPriority::Low => "Low".dimmed().to_string(),
        WorkItemPriority::None => String::from("—"),
    }
}
