use core::time;
use std::sync::Arc;

#[double]
use crate::db::DBTrans;
use crate::{
    language::{self, indefinite_article, join_words},
    message_handler::{
        user_commands::{user_error, UResult, UserError, VerbContext},
        ListenerSession,
    },
    models::{
        consent::ConsentType,
        item::{
            ActiveConversation, Buff, BuffCause, BuffImpact, ConversationIntensity,
            ConversationTopic, ConversationalInterestType, ConversationalStyle, Item, ItemFlag,
            SkillType, StatType,
        },
        journal::JournalType,
        task::{Task, TaskDetails, TaskMeta},
    },
    regular_tasks::{TaskHandler, TaskRunContext},
    services::skills::calculate_total_stats_skills_for_user,
    static_content::journals::award_journal_if_needed,
    DResult,
};
use ansi::ansi;
use async_trait::async_trait;
use chrono::Utc;
use itertools::Itertools;
use mockall_double::double;
use nom::{
    branch::alt,
    bytes::complete::tag,
    character::complete::space1,
    combinator::{cut, map, success},
    error::{context, VerboseError, VerboseErrorKind},
    sequence::{preceded, tuple},
};
use rand::{prelude::SliceRandom, Rng};

use super::{comms::broadcast_to_room, display::bar_n_of_m, skills::skill_check_and_grind};

struct ConversationResult {
    pub my_total_interest: u64,
    pub their_total_interest: u64,
    pub my_direct_interest: u64,
    pub my_skill_level: f64,
    pub my_transition_time: u64,
    pub their_skill_level: f64,
    pub their_transition_time: u64,
}

#[derive(Clone)]
pub struct ShareTaskHandler;
#[async_trait]
impl TaskHandler for ShareTaskHandler {
    async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult<Option<time::Duration>> {
        let (p1_type, p1_code) = ctx
            .task
            .meta
            .task_code
            .split_once("/")
            .ok_or("Bad share task code")?;
        let p1 = match ctx.trans.find_item_by_type_code(p1_type, p1_code).await? {
            None => return Ok(None),
            Some(v) => v,
        };
        let p1_conv = match p1.active_conversation.as_ref() {
            None => return Ok(None),
            Some(v) => v,
        };
        let (p2_type, p2_code) = p1_conv
            .partner_ref
            .split_once("/")
            .ok_or("Bad share partner")?;
        let p2 = match ctx.trans.find_item_by_type_code(p2_type, p2_code).await? {
            None => return Ok(None),
            Some(v) => v,
        };
        let p2_conv = match p2.active_conversation.as_ref() {
            None => return Ok(None),
            Some(v) => v,
        };

        let intensity_word = match p1_conv.current_intensity {
            ConversationIntensity::Fast => "quickly ",
            ConversationIntensity::Slow => "slowly ",
            ConversationIntensity::Normal => "",
        };

        let (pa, pb) = if rand::thread_rng().gen::<f32>() < 0.5 {
            (&p1, &p2)
        } else {
            (&p2, &p1)
        };

        let msg = match p1_conv.current_topic {
            ConversationTopic::ParodyKingsOffice => {
                let act = (&vec![
                    "mocks the existence of a self-styled king".to_owned(),
                    format!("declares {} king in a mocking tone", &pa.pronouns.intensive),
                    format!("puts on {} best Queens English and asks if {} would like some wasteland tea from {} finest rusty mug", &pa.pronouns.possessive, &pb.display_for_sentence(1, false), &pa.pronouns.possessive),
                    format!("utters, with no hint of fear, a phrase that would have {} up for les majeste if the king had any real power", &pa.pronouns.object)
                ]).choose(&mut rand::thread_rng()).unwrap().clone();
                let reaction = (&vec![
                    format!("can barely contain {} laughter", &pb.pronouns.possessive),
                    "thinks it is hilarious".to_owned(),
                    "finds it rather funny".to_owned(),
                    "is quite amused".to_owned(),
                    "snorts to suppress a laugh".to_owned(),
                ])
                    .choose(&mut rand::thread_rng())
                    .unwrap()
                    .clone();
                format!(
                    "{} {}{} and {} {}",
                    pa.display_for_sentence(1, true),
                    intensity_word,
                    &act,
                    &pb.display_for_sentence(1, false),
                    &reaction,
                )
            }
            ConversationTopic::PlayFight => {
                let act = *(&vec![
                    "pretends to throw a punch",
                    "engages in a playful wrestling match",
                    "swings an imaginary sword",
                    "throws a fake kick",
                ])
                    .choose(&mut rand::thread_rng())
                    .unwrap();
                let reaction = *(&vec![
                    "laughs it off",
                    "plays along with a grin",
                    "dodges the imaginary attack",
                    "feigns a dramatic injury",
                ])
                    .choose(&mut rand::thread_rng())
                    .unwrap();
                format!(
                    "{} {}{} and {} {}",
                    pa.display_for_sentence(1, true),
                    intensity_word,
                    &act,
                    &pb.display_for_sentence(1, false),
                    &reaction,
                )
            }
            ConversationTopic::ThoughtsOnSunTzu => {
                let thought = *(&vec![
                    "reflects on Sun Tzu's strategic brilliance",
                    "ponders Sun Tzu's timeless wisdom",
                    "recalls a favorite Sun Tzu quote",
                    "shares a strategic insight inspired by Sun Tzu",
                ])
                    .choose(&mut rand::thread_rng())
                    .unwrap();
                format!(
                    "{} {}{} and {} listens attentively",
                    pa.display_for_sentence(1, true),
                    intensity_word,
                    &thought,
                    &pb.display_for_sentence(1, false),
                )
            }
            ConversationTopic::ThoughtsOnMachiavelli => {
                let thought = *(&vec![
                    "discusses Machiavelli's political theories",
                    "evaluates the relevance of Machiavellian principles",
                    "analyzes Machiavelli's views on power",
                    "ponders the ethics of Machiavellian strategies",
                ])
                    .choose(&mut rand::thread_rng())
                    .unwrap();
                format!(
                    "{} {}{} and {} nods in agreement",
                    pa.display_for_sentence(1, true),
                    intensity_word,
                    &thought,
                    &pb.display_for_sentence(1, false),
                )
            }
            ConversationTopic::ExploringRuins => {
                let exploration = *(&vec![
                    "describes the eerie atmosphere of ruined buildings",
                    "shares interesting findings from recent explorations",
                    "reminisces about a thrilling encounter in a ruin",
                    "suggests the best ruins for scavenging",
                ])
                    .choose(&mut rand::thread_rng())
                    .unwrap();
                format!(
                    "{} {}{} and {} listens intently",
                    pa.display_for_sentence(1, true),
                    intensity_word,
                    &exploration,
                    &pb.display_for_sentence(1, false),
                )
            }
            ConversationTopic::RoamingEnemies => {
                let enemy_tales = *(&vec![
                    "recounts a close encounter with a formidable enemy",
                    "discusses strategies for dealing with roaming threats",
                    "shares tips on identifying dangerous foes",
                    "warns about the most treacherous areas for enemies",
                ])
                    .choose(&mut rand::thread_rng())
                    .unwrap();
                format!(
                    "{} {}{} and {} nods with newfound caution",
                    pa.display_for_sentence(1, true),
                    intensity_word,
                    &enemy_tales,
                    &pb.display_for_sentence(1, false),
                )
            }
            ConversationTopic::FishingSpots => {
                let fishing_info = *(&vec![
                    "reveals secret fishing spots with the best catches",
                    "discusses the ideal bait for different fishing spots",
                    "shares amusing anecdotes from fishing adventures",
                    "boasts about the biggest fish caught in a particular spot",
                ])
                    .choose(&mut rand::thread_rng())
                    .unwrap();
                format!(
                    "{} {}{} and {} expresses interest",
                    pa.display_for_sentence(1, true),
                    intensity_word,
                    &fishing_info,
                    &pb.display_for_sentence(1, false),
                )
            }
            ConversationTopic::GoodAmbushSpots => {
                let ambush_strategy = *(&vec![
                    "reveals the best spots for surprise ambushes",
                    "discusses tactics for setting up successful ambushes",
                    "describes the terrain for effective surprise attacks",
                    "shares personal experiences with ambush strategies",
                ])
                    .choose(&mut rand::thread_rng())
                    .unwrap();
                format!(
                    "{} {}{} and {} listens with a thoughtful expression",
                    pa.display_for_sentence(1, true),
                    intensity_word,
                    &ambush_strategy,
                    &pb.display_for_sentence(1, false),
                )
            }
            ConversationTopic::SurvivingWeather => {
                let weather_survival = *(&vec![
                    "shares tips on surviving harsh weather conditions",
                    "discusses the best clothing for different climates",
                    "recounts a daring adventure in extreme weather",
                    "offers advice on weather-related challenges",
                ])
                    .choose(&mut rand::thread_rng())
                    .unwrap();
                format!(
                    "{} {}{} and {} nods appreciatively",
                    pa.display_for_sentence(1, true),
                    intensity_word,
                    &weather_survival,
                    &pb.display_for_sentence(1, false),
                )
            }
        };
        let msg = format!(ansi!("<magenta>{}<reset>\n"), &msg);
        broadcast_to_room(&ctx.trans, &p1.location, None, &msg).await?;

        let mut p1_mut = (*p1).clone();
        let mut p2_mut = (*p2).clone();

        for (interest, mut growth) in topic_to_interest_growth(&p1_conv.current_topic) {
            growth *= 100;
            if growth > 0 {
                match p1_conv.current_intensity {
                    ConversationIntensity::Fast => {
                        growth *= 6;
                        growth /= 2;
                    }
                    ConversationIntensity::Slow => {
                        growth *= 4;
                        growth /= 3;
                    }
                    ConversationIntensity::Normal => {
                        growth *= 2;
                    }
                }
            } else {
                growth *= 2;
            }
            p1_mut.active_conversation.as_mut().map(|ac| {
                ac.interest_levels
                    .entry(interest.clone())
                    .and_modify(|v| (*v) = ((*v) as i64 + growth).max(0).min(10000) as u64)
                    .or_insert(growth.max(0) as u64)
            });
            p2_mut.active_conversation.as_mut().map(|ac| {
                ac.interest_levels
                    .entry(interest.clone())
                    .and_modify(|v| (*v) = ((*v) as i64 + growth).max(0).min(10000) as u64)
                    .or_insert(growth.max(0) as u64)
            });
        }

        let res = compute_conversation_result(&p1, &p1_conv, &p2, &p2_conv);
        p1_mut.active_conversation.as_mut().map(|ac| {
            ac.peak_total_interest = res.my_total_interest.max(ac.peak_total_interest);
        });
        p2_mut.active_conversation.as_mut().map(|ac| {
            ac.peak_total_interest = res.their_total_interest.max(ac.peak_total_interest);
        });
        let peak_reached_interest: Option<ConversationalInterestType> =
            p1_mut.active_conversation.as_ref().and_then(|ac| {
                ac.interest_levels
                    .iter()
                    .find(|(_, lev)| **lev >= 10000)
                    .map(|(it, _)| it.clone())
            });
        if let Some(peak_reached_interest) = peak_reached_interest {
            ctx.trans.save_item_model(&p2_mut).await?;
            stop_conversation_mut(
                &ctx.trans,
                &mut p1_mut,
                &format!(
                    "has had {} fill of talk about {} so puts a stop to the conversation with",
                    &p1.pronouns.possessive,
                    peak_reached_interest.display()
                ),
            )
            .await?;
            ctx.trans.save_item_model(&p1_mut).await?;
            return Ok(None);
        }

        let p1_ago = (Utc::now() - p1_conv.last_change).num_seconds();
        if p1_ago >= (res.my_transition_time as i64)
            && p1_ago - 5 < (res.my_transition_time as i64)
            && p1.item_type == "player"
        {
            if let Some((sess, _)) = ctx.trans.find_session_for_player(&p1.item_code).await? {
                inform_player_convo_change_ready(&ctx.trans, &p1_conv, &sess).await?;
            }
        }
        let p2_ago = (Utc::now() - p2_conv.last_change).num_seconds();
        if p2_ago >= (res.their_transition_time as i64)
            && p2_ago - 5 < (res.their_transition_time as i64)
            && p2.item_type == "player"
        {
            if let Some((sess, _)) = ctx.trans.find_session_for_player(&p2.item_code).await? {
                inform_player_convo_change_ready(&ctx.trans, &p2_conv, &sess).await?;
            }
        }

        ctx.trans.save_item_model(&p1_mut).await?;
        ctx.trans.save_item_model(&p2_mut).await?;

        Ok(Some(time::Duration::from_millis(5000)))
    }
}
pub static TASK_HANDLER: &(dyn TaskHandler + Sync + Send) = &ShareTaskHandler;

async fn inform_player_convo_change_ready(
    trans: &DBTrans,
    convo: &ActiveConversation,
    sess: &ListenerSession,
) -> DResult<()> {
    let intensity_commands = vec![
        ConversationIntensity::Slow,
        ConversationIntensity::Normal,
        ConversationIntensity::Fast,
    ]
    .into_iter()
    .filter(|ci| ci != &convo.current_intensity)
    .map(|ci| format!(ansi!("<bold>{}<reset>"), ci.to_command()))
    .join(" or ");

    let topics = style_to_allowed_topics(&convo.style)
        .into_iter()
        .filter(|t| t != &convo.current_topic)
        .map(|t| format!(ansi!("<bold>{}<reset>"), t.display_command()))
        .join(" or ");

    let styles = vec![
        ConversationalStyle::Amicable,
        ConversationalStyle::Joking,
        ConversationalStyle::Serious,
    ]
    .into_iter()
    .filter(|s| s != &convo.style)
    .map(|ci| format!(ansi!("<bold>{}<reset>"), ci.display()))
    .join(" or ");

    trans
        .queue_for_session(
            sess,
            Some(&format!(ansi!(
                "It's been long enough that you feel you'd have a good shot at changing the pace \
                 (try {}), topic (try {}) or style (try {}) of the conversation. \
                 [use <bold>share status<reset> to check interest levels, and <bold>help share<reset> \
                 to learn more]\n"),
                intensity_commands,
                topics,
                styles,
            )),
        )
        .await?;
    Ok(())
}

fn share_skill_to_base_transition_time(skill: f64) -> f64 {
    ((13.6666666666666666666667 - skill) * 3.0)
        .max(1.0)
        .min(23.0)
}

fn compute_conversation_result(
    me: &Item,
    my_conversation: &ActiveConversation,
    them: &Item,
    their_conversation: &ActiveConversation,
) -> ConversationResult {
    let my_direct_interest = my_conversation
        .interest_levels
        .iter()
        .map(|(_, pl)| pl)
        .sum();
    let their_direct_interest: u64 = their_conversation
        .interest_levels
        .iter()
        .map(|(_, pl)| pl)
        .sum();

    let my_theoretical_skill_level: f64 = *(me.total_skills.get(&SkillType::Share).unwrap_or(&8.0));
    let their_theoretical_skill_level: f64 =
        *(them.total_skills.get(&SkillType::Share).unwrap_or(&8.0));

    let my_transition_time = share_skill_to_base_transition_time(my_theoretical_skill_level) as u64;
    let their_transition_time =
        share_skill_to_base_transition_time(their_theoretical_skill_level) as u64;

    let my_total_interest = my_direct_interest + 1363 * (their_transition_time - 1);
    let their_total_interest = their_direct_interest + 1363 * (my_transition_time - 1);

    ConversationResult {
        my_total_interest,
        their_total_interest,
        my_direct_interest,
        my_skill_level: my_theoretical_skill_level,
        my_transition_time,
        their_skill_level: their_theoretical_skill_level,
        their_transition_time,
    }
}

fn relevant_interest_types(_character: &Item) -> Vec<ConversationalInterestType> {
    // Static for now
    vec![
        ConversationalInterestType::Philosophy,
        ConversationalInterestType::LocalGeography,
        ConversationalInterestType::Threats,
        ConversationalInterestType::Tactics,
        ConversationalInterestType::Weather,
        ConversationalInterestType::Politics,
        ConversationalInterestType::Frivolity,
    ]
}

fn append_interest_graph(msg: &mut String, character: &Item, conversation: &ActiveConversation) {
    for pt in relevant_interest_types(character) {
        let level = *(conversation.interest_levels.get(&pt).unwrap_or(&0));
        msg.push_str(&format!(
            ansi!("{}:\t{}<bold>[<reset><magenta><bgblack>{}<reset><bold>]<reset>\n"),
            pt.display(),
            if pt.display().len() >= 15 { "" } else { "\t" },
            // 10,000 levels, 25 divisions 10000/25 => 400 per bar.
            bar_n_of_m(level / 400, 25)
        ));
    }
}

pub async fn display_conversation_status(
    trans: &DBTrans,
    to_whom: &Item,
    with_whom: &Item,
) -> DResult<()> {
    let conversation = match to_whom.active_conversation.as_ref() {
        None => return Ok(()),
        Some(v) => v,
    };
    let partner_conversation = match with_whom.active_conversation.as_ref() {
        None => return Ok(()),
        Some(v) => v,
    };

    let result =
        compute_conversation_result(&to_whom, &conversation, &with_whom, &partner_conversation);
    let mut msg = format!(ansi!("The current conversational style is {}. Type the name of one of the following alternative styles to switch and open different topics: <bold>{}<reset>\n"),
                          conversation.style.display(),
                          conversation.style.transitions().iter().map(|v| v.display()).join(" "));
    let alt_topics: Vec<String> = style_to_allowed_topics(&conversation.style)
        .into_iter()
        .filter(|t| t != &conversation.current_topic)
        .map(|t| {
            format!(
                ansi!("{} (type <bold>{}<reset>)"),
                t.display_readable(),
                t.display_command()
            )
        })
        .collect();
    msg.push_str(&format!("The current topic is {}. Your current style allows switching to the following topics: {}\n", conversation.current_topic.display_readable(), &alt_topics.join(" or ")));
    msg.push_str("Your current interest levels:\n");
    append_interest_graph(&mut msg, to_whom, conversation);
    msg.push_str(&format!(
        "Total direct interest: {}\n",
        (result.my_direct_interest as f64 / 100.0).round()
    ));
    let partner_txt = with_whom.display_for_sentence(1, false);
    if result.their_skill_level < result.my_skill_level {
        msg.push_str(&format!("Your interest level is increased as you observe how {} is learning despite being a less skilled conversationalist than you.\n", &partner_txt));
    }

    if to_whom.item_type == "player" {
        if let Some((sess, _)) = trans.find_session_for_player(&to_whom.item_code).await? {
            trans.queue_for_session(&sess, Some(&msg)).await?;
        }
    }
    Ok(())
}

// Designed to be neutral as to whether it is p1 or p2.
fn share_event_name(p1: &Item, p2: &Item) -> String {
    let canon_it = if p1.item_code < p2.item_code
        || (p1.item_code == p2.item_code && p1.item_type < p2.item_type)
    {
        p1
    } else {
        p2
    };
    canon_it.refstr()
}

pub async fn start_conversation(
    trans: &'_ DBTrans,
    initiator: &'_ Item,
    acceptor: &'_ Item,
) -> UResult<()> {
    if acceptor.item_code == initiator.item_code && acceptor.item_type == initiator.item_type {
        user_error("That's you... and that would make you feel a bit lonely.".to_owned())?;
    }

    if acceptor.item_type == "player" {
        if initiator.item_type != "player" {
            user_error("Only players can initiate conversation with players".to_owned())?;
        }
        let _ =
            trans
                .find_session_for_player(&acceptor.item_code)
                .await?
                .ok_or_else(|| {
                    UserError(format!("You propose sharing knowledge with {}, but {} doesn't seem interested. [That player is currently not logged in]",
                                      acceptor.display_for_sentence(1, false),
                                      &acceptor.pronouns.subject
                    ))
                })?;
        trans.delete_expired_user_consent().await?;
        trans
            .find_user_consent_by_parties_type(
                &acceptor.item_code,
                &initiator.item_code,
                &ConsentType::Share,
            )
            .await?
            .ok_or_else(|| {
                UserError(format!(
                    ansi!(
                        "You ask {} to share knowledge, but {} doesn't \
                                             seem interested. [The other player will need to type \
                                             <bold>allow share from {}<reset> before their \
                                             character will consent to knowledge sharing]"
                    ),
                    &acceptor.display_for_sentence(1, false),
                    &acceptor.pronouns.subject,
                    initiator.display_for_sentence(1, false)
                ))
            })?;
    } else {
        if !acceptor.flags.contains(&ItemFlag::AllowShare) {
            user_error(format!(
                "You ask {} to share knowledge with you, but {} doesn't seem interested.",
                &acceptor.display_for_sentence(1, false),
                &acceptor.pronouns.subject
            ))?;
        }
    }

    if !initiator.queue.is_empty() {
        user_error(ansi!("You're a bit busy right now! [Use the <bold>stop<reset> command to stop what you are doing].").to_owned())?;
    }

    if initiator
        .active_combat
        .as_ref()
        .map(|ac| ac.attacking.is_some())
        .unwrap_or(false)
    {
        user_error(
            "You can share knowledge, or you can fight... but both at once seems like too much!"
                .to_owned(),
        )?;
    }

    if !acceptor.queue.is_empty()
        || acceptor
            .active_combat
            .as_ref()
            .map(|ac| ac.attacking.is_some())
            .unwrap_or(false)
    {
        user_error(format!(
            "{} seems to be a bit busy right now!",
            acceptor.display_for_sentence(1, true)
        ))?;
    }

    if acceptor.active_conversation.is_some() {
        user_error(format!(
            "{} seems to be already deep in conversation!",
            acceptor.display_for_sentence(1, true)
        ))?;
    }

    broadcast_to_room(
        trans,
        &initiator.location,
        None,
        &format!(
            ansi!("<magenta>{} proposes to share knowledge with {}, and {} accepts!<reset>\n"),
            &initiator.display_for_sentence(1, true),
            &acceptor.display_for_sentence(1, true),
            &acceptor.pronouns.subject
        ),
    )
    .await?;

    let mut acceptor_mut = (*acceptor).clone();
    let mut initiator_mut = (*initiator).clone();
    acceptor_mut.active_conversation = Some(ActiveConversation {
        partner_ref: initiator.refstr(),
        last_change: Utc::now(),
        ..Default::default()
    });
    trans.save_item_model(&acceptor_mut).await?;
    initiator_mut.active_conversation = Some(ActiveConversation {
        partner_ref: acceptor.refstr(),
        last_change: Utc::now(),
        ..Default::default()
    });
    trans.save_item_model(&initiator_mut).await?;

    trans
        .upsert_task(&Task {
            meta: TaskMeta {
                task_code: share_event_name(&initiator, &acceptor),
                next_scheduled: Utc::now() + chrono::TimeDelta::try_milliseconds(5000).unwrap(),
                ..Default::default()
            },
            details: TaskDetails::ShareTick,
        })
        .await?;

    Ok(())
}

async fn apply_conversation_buff(trans: &DBTrans, to_player: &mut Item) -> DResult<()> {
    let peak = to_player
        .active_conversation
        .as_ref()
        .map(|ac| ac.peak_total_interest)
        .unwrap_or(0);
    let mut buffs: Vec<BuffImpact> = vec![];
    const BASE: u64 = 7691;
    if peak >= BASE * 7 {
        buffs.push(BuffImpact::ChangeStat {
            stat: StatType::Cool,
            magnitude: 2.0,
        });
    } else if peak >= BASE {
        buffs.push(BuffImpact::ChangeStat {
            stat: StatType::Cool,
            magnitude: 1.0,
        });
    }
    if peak >= BASE * 8 {
        buffs.push(BuffImpact::ChangeStat {
            stat: StatType::Brains,
            magnitude: 2.0,
        });
    } else if peak >= BASE * 2 {
        buffs.push(BuffImpact::ChangeStat {
            stat: StatType::Brains,
            magnitude: 1.0,
        });
    }
    if peak >= BASE * 9 {
        buffs.push(BuffImpact::ChangeStat {
            stat: StatType::Senses,
            magnitude: 2.0,
        });
    } else if peak >= BASE * 3 {
        buffs.push(BuffImpact::ChangeStat {
            stat: StatType::Senses,
            magnitude: 1.0,
        });
    }
    if peak >= BASE * 10 {
        buffs.push(BuffImpact::ChangeStat {
            stat: StatType::Endurance,
            magnitude: 2.0,
        });
    } else if peak >= BASE * 4 {
        buffs.push(BuffImpact::ChangeStat {
            stat: StatType::Endurance,
            magnitude: 1.0,
        });
    }
    if peak >= BASE * 11 {
        buffs.push(BuffImpact::ChangeStat {
            stat: StatType::Brawn,
            magnitude: 2.0,
        });
    } else if peak >= BASE * 5 {
        buffs.push(BuffImpact::ChangeStat {
            stat: StatType::Brawn,
            magnitude: 1.0,
        });
    }
    if peak >= BASE * 12 {
        buffs.push(BuffImpact::ChangeStat {
            stat: StatType::Reflexes,
            magnitude: 2.0,
        });
    } else if peak >= BASE * 6 {
        buffs.push(BuffImpact::ChangeStat {
            stat: StatType::Reflexes,
            magnitude: 1.0,
        });
    }

    let exp_task_code = format!("sharing/{}/{}", &to_player.item_type, &to_player.item_code);

    to_player.temporary_buffs.retain(|b| match b.cause {
        BuffCause::WaitingTask {
            ref task_type,
            ref task_code,
        } if task_type == "ExpireBuff" && task_code == &exp_task_code => true,
        _ => false,
    });
    to_player.temporary_buffs.push(Buff {
        description: "Sharing knowledge improved you (for a while)".to_owned(),
        code: "sharIng".to_owned(),
        cause: BuffCause::WaitingTask {
            task_type: "ExpireBuff".to_owned(),
            task_code: exp_task_code.clone(),
        },
        impacts: buffs.clone(),
    });
    trans
        .upsert_task(&Task {
            meta: TaskMeta {
                task_code: exp_task_code.clone(),
                next_scheduled: Utc::now() + chrono::TimeDelta::try_seconds(600).unwrap(),
                ..Default::default()
            },
            details: TaskDetails::ExpireBuff {
                item: to_player.refstr(),
                code: "sharing".to_owned(),
            },
        })
        .await?;

    if to_player.item_type == "player" {
        if let Some(user) = trans.find_by_username(&to_player.item_code).await? {
            calculate_total_stats_skills_for_user(to_player, &user);
        }

        if let Some((sess, _)) = trans.find_session_for_player(&to_player.item_code).await? {
            let impacts_vec = buffs
                .iter()
                .filter_map(|b| match b {
                    BuffImpact::ChangeStat { stat, magnitude } => {
                        Some(format!("{} is increased by {}", stat.display(), magnitude))
                    }
                    _ => None,
                })
                .collect::<Vec<String>>();
            let impact = join_words(
                &impacts_vec
                    .iter()
                    .map(|b| b.as_str())
                    .collect::<Vec<&str>>(),
            );

            trans
                .queue_for_session(
                    &sess,
                    Some(&format!(ansi!(
                        "<green>You feel sharing knowledge improved you [for the next 10 minutes, {}]<reset>\n"),
                        impact
                    )),
                )
                .await?;
        }
    }

    Ok(())
}

pub async fn stop_conversation_mut(
    trans: &DBTrans,
    participant: &mut Item,
    // Should make sense with actor first and other partner after. e.g. walks away from conversation with
    leave_description: &str,
) -> DResult<()> {
    let (partner_type, partner_code) = match participant
        .active_conversation
        .as_ref()
        .and_then(|ac| ac.partner_ref.split_once("/"))
    {
        None => return Ok(()),
        Some(v) => v,
    };
    let partner = match trans
        .find_item_by_type_code(partner_type, partner_code)
        .await?
    {
        None => return Ok(()),
        Some(v) => v,
    };
    let mut partner_mut = (*partner).clone();

    apply_conversation_buff(trans, &mut partner_mut).await?;
    apply_conversation_buff(trans, participant).await?;

    participant.active_conversation = None;
    partner_mut.active_conversation = None;

    if participant.item_type == "player" && partner_mut.item_type == "player" {
        if let Some(mut participant_user) = trans.find_by_username(&participant.item_code).await? {
            if award_journal_if_needed(
                trans,
                &mut participant_user,
                participant,
                JournalType::SharedWithPlayer,
            )
            .await?
            {
                trans.save_user_model(&participant_user).await?;
            }
        }
        if let Some(mut partner_user) = trans.find_by_username(&partner.item_code).await? {
            if award_journal_if_needed(
                trans,
                &mut partner_user,
                &mut partner_mut,
                JournalType::SharedWithPlayer,
            )
            .await?
            {
                trans.save_user_model(&partner_user).await?;
            }
        }
    }

    trans.save_item_model(&partner_mut).await?;

    broadcast_to_room(
        trans,
        &participant.location,
        None,
        &format!(
            ansi!("<magenta>{} {} {}.<reset>\n"),
            &participant.display_for_sentence(1, true),
            leave_description,
            &partner_mut.display_for_sentence(1, false)
        ),
    )
    .await?;

    Ok(())
}

pub fn parse_conversation_topic(input: &str) -> Result<Option<ConversationTopic>, &'static str> {
    let r = alt((
        map(
            tuple((
                tag("parody"),
                space1::<&str, VerboseError<&str>>,
                cut(context(
                    ansi!("Try <bold>parody kings office<reset>"),
                    tuple((tag("kings"), space1, tag("office"))),
                )),
            )),
            |_| Some(ConversationTopic::ParodyKingsOffice),
        ),
        map(
            tuple((
                tag("play"),
                space1::<&str, VerboseError<&str>>,
                cut(context(ansi!("Try <bold>play fight<reset>"), tag("fight"))),
            )),
            |_| Some(ConversationTopic::PlayFight),
        ),
        preceded(
            tuple((tag("thoughts"), space1::<&str, VerboseError<&str>>)),
            cut(context(
                ansi!(
                    "Try <bold>thoughts on machiavelli<reset> or <bold>thoughts on sun tzu<reset>"
                ),
                preceded(
                    tuple((tag("on"), space1)),
                    alt((
                        map(
                            preceded(
                                tuple((tag("sun"), space1)),
                                cut(context(
                                    ansi!("Try <bold>thoughts on sun tzu<reset>"),
                                    tag("tzu"),
                                )),
                            ),
                            |_| Some(ConversationTopic::ThoughtsOnSunTzu),
                        ),
                        map(tag("machiavelli"), |_| {
                            Some(ConversationTopic::ThoughtsOnMachiavelli)
                        }),
                    )),
                ),
            )),
        ),
        map(
            tuple((
                tag("exploring"),
                space1::<&str, VerboseError<&str>>,
                cut(context(
                    ansi!("Try <bold>exploring ruins<reset>"),
                    tag("ruins"),
                )),
            )),
            |_| Some(ConversationTopic::ExploringRuins),
        ),
        map(
            tuple((
                tag("roaming"),
                space1::<&str, VerboseError<&str>>,
                cut(context(
                    ansi!("Try <bold>roaming enemies<reset>"),
                    tag("enemies"),
                )),
            )),
            |_| Some(ConversationTopic::RoamingEnemies),
        ),
        map(
            tuple((
                tag("fishing"),
                space1::<&str, VerboseError<&str>>,
                cut(context(
                    ansi!("Try <bold>fishing spots<reset>"),
                    tag("spots"),
                )),
            )),
            |_| Some(ConversationTopic::FishingSpots),
        ),
        map(
            tuple((
                tag("good"),
                space1::<&str, VerboseError<&str>>,
                cut(context(
                    ansi!("Try <bold>good ambush spots<reset>"),
                    tuple((tag("ambush"), space1, tag("spots"))),
                )),
            )),
            |_| Some(ConversationTopic::GoodAmbushSpots),
        ),
        map(
            tuple((
                tag("surviving"),
                space1::<&str, VerboseError<&str>>,
                cut(context(
                    ansi!("Try <bold>surviving weather<reset>"),
                    tag("weather"),
                )),
            )),
            |_| Some(ConversationTopic::SurvivingWeather),
        ),
        success(None),
    ))(input);
    const CATCHALL_ERROR: &str = "Invalid command";
    match r {
        Ok((_, result)) => Ok(result),
        Err(nom::Err::Error(e)) | Err(nom::Err::Failure(e)) => Err(e
            .errors
            .into_iter()
            .find_map(|k| match k.1 {
                VerboseErrorKind::Context(s) => Some(s),
                _ => None,
            })
            .unwrap_or(CATCHALL_ERROR)),
        Err(_) => Err(CATCHALL_ERROR),
    }
}

async fn check_conversation_change<'l>(
    ctx: &VerbContext<'_>,
    player_item: &'l mut Item,
) -> UResult<Option<(&'l mut ActiveConversation, Arc<Item>)>> {
    let my_convo = player_item
        .active_conversation
        .as_ref()
        .ok_or_else(|| UserError("You're not in a conversation".to_owned()))?;

    let how_long_ago = (Utc::now() - my_convo.last_change).num_seconds();
    if how_long_ago < 1 {
        user_error(
            "You can't keep up with the rate of change in the conversation [try again later]"
                .to_owned(),
        )?;
    }
    // Changing topic after 20s is 50/50 at level 7, level needed increases for every 3s less.
    let level = 13.6666666666666666666667 - (how_long_ago as f64) / 3.0;
    let result = skill_check_and_grind(ctx.trans, player_item, &SkillType::Share, level).await?;
    if result < -0.5 {
        // Mostly to prevent spamming until they succeed, have a chance of resetting timer...
        if let Some(ac) = player_item.active_conversation.as_mut() {
            ac.last_change = Utc::now();
        }
        ctx.trans.queue_for_session(&ctx.session, Some(ansi!("<red>Your attempt to change the conversation goes so awkwardly you not only fail, but feel you'll have to wait longer now to have another shot!<reset>\n"))).await?;
        return Ok(None);
    } else if result < 0.0 {
        ctx.trans
            .queue_for_session(
                &ctx.session,
                Some(ansi!(
                    "<magenta>Your attempt to change the conversation doesn't pan out.<reset>\n"
                )),
            )
            .await?;
        return Ok(None);
    }

    // This way lets us pass the borrow checker with the skillcheck.
    let my_convo = player_item
        .active_conversation
        .as_mut()
        .ok_or_else(|| UserError("You're not in a conversation".to_owned()))?;
    let player_info = my_convo
        .partner_ref
        .split_once("/")
        .ok_or_else(|| UserError("Can't get partner ref".to_owned()))?;
    let other_player = ctx
        .trans
        .find_item_by_type_code(player_info.0, player_info.1)
        .await?
        .ok_or_else(|| UserError("Partner is gone!".to_owned()))?;

    return Ok(Some((my_convo, other_player)));
}

fn style_to_initial_topic(style: &ConversationalStyle) -> ConversationTopic {
    match style {
        ConversationalStyle::Amicable => ConversationTopic::FishingSpots,
        ConversationalStyle::Serious => ConversationTopic::RoamingEnemies,
        ConversationalStyle::Joking => ConversationTopic::ParodyKingsOffice,
    }
}

fn style_to_allowed_topics(style: &ConversationalStyle) -> Vec<ConversationTopic> {
    match style {
        ConversationalStyle::Amicable => vec![
            ConversationTopic::FishingSpots,
            ConversationTopic::ThoughtsOnSunTzu,
            ConversationTopic::ExploringRuins,
        ],
        ConversationalStyle::Joking => vec![
            ConversationTopic::ParodyKingsOffice,
            ConversationTopic::PlayFight,
            ConversationTopic::ThoughtsOnMachiavelli,
        ],
        ConversationalStyle::Serious => vec![
            ConversationTopic::RoamingEnemies,
            ConversationTopic::SurvivingWeather,
            ConversationTopic::GoodAmbushSpots,
        ],
    }
}

fn topic_to_allowed_style(topic: &ConversationTopic) -> ConversationalStyle {
    match topic {
        ConversationTopic::FishingSpots => ConversationalStyle::Amicable,
        ConversationTopic::ThoughtsOnSunTzu => ConversationalStyle::Amicable,
        ConversationTopic::ExploringRuins => ConversationalStyle::Amicable,
        ConversationTopic::ParodyKingsOffice => ConversationalStyle::Joking,
        ConversationTopic::PlayFight => ConversationalStyle::Joking,
        ConversationTopic::ThoughtsOnMachiavelli => ConversationalStyle::Joking,
        ConversationTopic::RoamingEnemies => ConversationalStyle::Serious,
        ConversationTopic::SurvivingWeather => ConversationalStyle::Serious,
        ConversationTopic::GoodAmbushSpots => ConversationalStyle::Serious,
    }
}

fn topic_to_interest_growth(style: &ConversationTopic) -> Vec<(ConversationalInterestType, i64)> {
    use ConversationTopic::*;
    use ConversationalInterestType::*;
    match style {
        FishingSpots => vec![
            (Philosophy, 0),
            (LocalGeography, 2),
            (Threats, -1),
            (Tactics, -1),
            (Weather, 3),
            (Politics, -1),
            (Frivolity, 2),
        ],
        ThoughtsOnSunTzu => vec![
            (Philosophy, 2),
            (LocalGeography, -1),
            (Threats, -1),
            (Tactics, 4),
            (Weather, -1),
            (Politics, 3),
            (Frivolity, 0),
        ],
        ExploringRuins => vec![
            (Philosophy, 1),
            (LocalGeography, 4),
            (Threats, -1),
            (Tactics, -1),
            (Weather, 2),
            (Politics, -1),
            (Frivolity, 2),
        ],
        ParodyKingsOffice => vec![
            (Philosophy, 1),
            (LocalGeography, -1),
            (Threats, -1),
            (Tactics, -1),
            (Weather, -1),
            (Politics, 4),
            (Frivolity, 3),
        ],
        PlayFight => vec![
            (Philosophy, 1),
            (LocalGeography, -1),
            (Threats, 2),
            (Tactics, 4),
            (Weather, -1),
            (Politics, -1),
            (Frivolity, 2),
        ],
        ThoughtsOnMachiavelli => vec![
            (Philosophy, 4),
            (LocalGeography, -1),
            (Threats, -1),
            (Tactics, 2),
            (Weather, -1),
            (Politics, 4),
            (Frivolity, -1),
        ],
        RoamingEnemies => vec![
            (Philosophy, -1),
            (LocalGeography, 1),
            (Threats, 4),
            (Tactics, 1),
            (Weather, -1),
            (Politics, 0),
            (Frivolity, 0),
        ],
        SurvivingWeather => vec![
            (Philosophy, -1),
            (LocalGeography, 3),
            (Threats, 2),
            (Tactics, -1),
            (Weather, 5),
            (Politics, -1),
            (Frivolity, -1),
        ],
        GoodAmbushSpots => vec![
            (Philosophy, -1),
            (LocalGeography, 1),
            (Threats, 3),
            (Tactics, 3),
            (Weather, 2),
            (Politics, -1),
            (Frivolity, -1),
        ],
    }
}

pub async fn change_conversational_style(
    ctx: &VerbContext<'_>,
    player_item: &mut Item,
    style: ConversationalStyle,
) -> UResult<()> {
    if player_item
        .active_conversation
        .as_ref()
        .map(|ac| ac.style == style)
        .unwrap_or(false)
    {
        user_error(format!(
            "You're alreading talking in {} {} style!",
            indefinite_article(style.display()),
            style.display()
        ))?;
    }
    let (my_convo, other_player) = match check_conversation_change(ctx, player_item).await? {
        None => return Ok(()),
        Some(c) => c,
    };

    my_convo.last_change = Utc::now();
    my_convo.style = style.clone();
    let topic = style_to_initial_topic(&style);
    my_convo.current_topic = topic.clone();
    let mut other_player = (*other_player).clone();
    other_player
        .active_conversation
        .as_mut()
        .map(|other_convo| {
            other_convo.style = style.clone();
            other_convo.current_topic = topic.clone();
        });
    ctx.trans.save_item_model(&other_player).await?;

    let alt_topics: Vec<String> = style_to_allowed_topics(&style)
        .into_iter()
        .filter(|t| t != &topic)
        .map(|t| {
            format!(
                ansi!("{} (type <bold>{}<reset>)"),
                t.display_readable(),
                t.display_command()
            )
        })
        .collect();
    let alt_topics_str: String =
        language::join_words_or(&alt_topics.iter().map(|t| t.as_str()).collect::<Vec<&str>>());

    let msg = format!(ansi!(
        "{} changes the style of conversation with {} to be {}. The conversation drifts to <bold>{}<reset>, but you realise it could shift to {}\n"),
        player_item.display_for_sentence(1, true),
        other_player.display_for_sentence(1, false),
        style.display(),
        topic.display_readable(),
        &alt_topics_str
    );
    broadcast_to_room(&ctx.trans, &player_item.location, None, &msg).await?;

    Ok(())
}

pub async fn change_conversation_topic(
    ctx: &VerbContext<'_>,
    player_item: &mut Item,
    topic: ConversationTopic,
) -> UResult<()> {
    if player_item
        .active_conversation
        .as_ref()
        .map(|ac| ac.current_topic == topic)
        .unwrap_or(false)
    {
        user_error("That's already the topic!".to_owned())?;
    }

    let (my_convo, other_player) = match check_conversation_change(ctx, player_item).await? {
        None => return Ok(()),
        Some(c) => c,
    };

    let expected_style = topic_to_allowed_style(&topic);
    if my_convo.style != expected_style {
        user_error(format!(
            ansi!(
                "You need to switch to be talking in the <bold>{}<reset> style \
                                  before you can switch to {}."
            ),
            expected_style.display(),
            topic.display_readable()
        ))?;
    }

    let mut other_player = (*other_player).clone();
    my_convo.current_topic = topic.clone();
    my_convo.last_change = Utc::now();
    other_player.active_conversation.as_mut().map(|ac| {
        ac.current_topic = topic.clone();
    });
    ctx.trans.save_item_model(&other_player).await?;

    let msg = format!(
        "{} subtley changes the topic of conversation with {}, and it drifts to {}\n",
        player_item.display_for_sentence(1, true),
        other_player.display_for_sentence(1, false),
        topic.display_readable(),
    );
    broadcast_to_room(&ctx.trans, &player_item.location, None, &msg).await?;

    Ok(())
}

pub async fn change_conversation_intensity(
    ctx: &VerbContext<'_>,
    player_item: &mut Item,
    intensity: ConversationIntensity,
) -> UResult<()> {
    if player_item
        .active_conversation
        .as_ref()
        .map(|ac| ac.current_intensity == intensity)
        .unwrap_or(false)
    {
        user_error(format!(
            "You're already talking {}",
            intensity.display_readable()
        ))?;
    }
    let (my_convo, other_player) = match check_conversation_change(ctx, player_item).await? {
        None => return Ok(()),
        Some(c) => c,
    };

    my_convo.current_intensity = intensity.clone();
    my_convo.last_change = Utc::now();

    let mut other_player = (*other_player).clone();
    other_player
        .active_conversation
        .as_mut()
        .map(|ac| ac.current_intensity = intensity.clone());
    ctx.trans.save_item_model(&other_player).await?;

    let msg = format!(
        "You notice a change in the pace of the conversation started by {}, and soon picked up by {}. The conversation is now proceeding {}.\n",
        player_item.display_for_sentence(1, true),
        other_player.display_for_sentence(1, false),
        intensity.display_readable(),
    );
    broadcast_to_room(&ctx.trans, &player_item.location, None, &msg).await?;

    Ok(())
}

#[cfg(test)]
mod tests {
    use crate::models::item::ConversationTopic;

    use super::*;

    #[test]
    fn parse_conversation_topic_works() {
        let cases: Vec<(&str, Result<Option<ConversationTopic>, &str>)> = vec![
            (
                "parody  kings   office",
                Ok(Some(ConversationTopic::ParodyKingsOffice)),
            ),
            (
                "parody your face",
                Err(ansi!("Try <bold>parody kings office<reset>")),
            ),
            ("play fight", Ok(Some(ConversationTopic::PlayFight))),
            ("play fgight", Err(ansi!("Try <bold>play fight<reset>"))),
            (
                "thoughts on sun 	tzu",
                Ok(Some(ConversationTopic::ThoughtsOnSunTzu)),
            ),
            (
                "thoughts on sun sets",
                Err(ansi!("Try <bold>thoughts on sun tzu<reset>")),
            ),
            (
                "thoughts  on machiavelli",
                Ok(Some(ConversationTopic::ThoughtsOnMachiavelli)),
            ),
            (
                "thoughts non machiavelli",
                Err(ansi!(
                    "Try <bold>thoughts on machiavelli<reset> or <bold>thoughts on sun tzu<reset>"
                )),
            ),
            (
                "exploring ruins",
                Ok(Some(ConversationTopic::ExploringRuins)),
            ),
            (
                "exploring roads",
                Err(ansi!("Try <bold>exploring ruins<reset>")),
            ),
            (
                "roaming enemies ",
                Ok(Some(ConversationTopic::RoamingEnemies)),
            ),
            (
                "roaming villains",
                Err(ansi!("Try <bold>roaming enemies<reset>")),
            ),
            ("fishing spots", Ok(Some(ConversationTopic::FishingSpots))),
            (
                "fishing sports",
                Err(ansi!("Try <bold>fishing spots<reset>")),
            ),
            (
                "good  ambush spots",
                Ok(Some(ConversationTopic::GoodAmbushSpots)),
            ),
            (
                "good ambush places",
                Err(ansi!("Try <bold>good ambush spots<reset>")),
            ),
            ("say hello world", Ok(None)),
        ];
        for case in cases {
            assert_eq!(parse_conversation_topic(case.0), case.1);
        }
    }

    #[test]
    fn style_to_topic_to_style_roundtrips() {
        use ConversationalStyle::*;
        for style in vec![Serious, Amicable, Joking] {
            for topic in style_to_allowed_topics(&style) {
                assert_eq!(topic_to_allowed_style(&topic), style);
            }
        }
    }
}