use anyhow::{bail, Context, Result};
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::time::Duration;

pub mod ollama;
pub mod openai;
pub mod anthropic;
pub mod kimi;
pub mod deepseek;
pub mod openrouter;

pub use ollama::OllamaClient;
pub use openai::OpenAiClient;
pub use anthropic::AnthropicClient;
pub use kimi::KimiClient;
pub use deepseek::DeepSeekClient;
pub use openrouter::OpenRouterClient;

/// LLM provider trait
#[async_trait]
pub trait LlmProvider: Send + Sync {
    /// Generate text from prompt
    async fn generate(&self, prompt: &str) -> Result<String>;
    
    /// Generate with system prompt
    async fn generate_with_system(&self, system: &str, user: &str) -> Result<String>;
    
    /// Check if provider is available
    async fn is_available(&self) -> bool;
    
    /// Get provider name
    fn name(&self) -> &str;
}

/// LLM client that wraps different providers
pub struct LlmClient {
    provider: Box<dyn LlmProvider>,
    config: LlmClientConfig,
}

#[derive(Debug, Clone)]
pub struct LlmClientConfig {
    pub max_tokens: u32,
    pub temperature: f32,
    pub timeout: Duration,
}

impl Default for LlmClientConfig {
    fn default() -> Self {
        Self {
            max_tokens: 500,
            temperature: 0.7,
            timeout: Duration::from_secs(30),
        }
    }
}

impl LlmClient {
    /// Create LLM client from configuration
    pub async fn from_config(config: &crate::config::LlmConfig) -> Result<Self> {
        let client_config = LlmClientConfig {
            max_tokens: config.max_tokens,
            temperature: config.temperature,
            timeout: Duration::from_secs(config.timeout),
        };

        let provider: Box<dyn LlmProvider> = match config.provider.as_str() {
            "ollama" => {
                Box::new(OllamaClient::new(&config.ollama.url, &config.ollama.model))
            }
            "openai" => {
                let api_key = config.openai.api_key.as_ref()
                    .ok_or_else(|| anyhow::anyhow!("OpenAI API key not configured"))?;
                Box::new(OpenAiClient::new(
                    &config.openai.base_url,
                    api_key,
                    &config.openai.model,
                )?)
            }
            "anthropic" => {
                let api_key = config.anthropic.api_key.as_ref()
                    .ok_or_else(|| anyhow::anyhow!("Anthropic API key not configured"))?;
                Box::new(AnthropicClient::new(api_key, &config.anthropic.model)?)
            }
            "kimi" => {
                let api_key = config.kimi.api_key.as_ref()
                    .ok_or_else(|| anyhow::anyhow!("Kimi API key not configured"))?;
                Box::new(KimiClient::with_base_url(api_key, &config.kimi.model, &config.kimi.base_url)?)
            }
            "deepseek" => {
                let api_key = config.deepseek.api_key.as_ref()
                    .ok_or_else(|| anyhow::anyhow!("DeepSeek API key not configured"))?;
                Box::new(DeepSeekClient::with_base_url(api_key, &config.deepseek.model, &config.deepseek.base_url)?)
            }
            "openrouter" => {
                let api_key = config.openrouter.api_key.as_ref()
                    .ok_or_else(|| anyhow::anyhow!("OpenRouter API key not configured"))?;
                Box::new(OpenRouterClient::with_base_url(api_key, &config.openrouter.model, &config.openrouter.base_url)?)
            }
            _ => bail!("Unknown LLM provider: {}", config.provider),
        };

        Ok(Self {
            provider,
            config: client_config,
        })
    }

    /// Create with specific provider
    pub fn with_provider(provider: Box<dyn LlmProvider>) -> Self {
        Self {
            provider,
            config: LlmClientConfig::default(),
        }
    }

    /// Generate commit message from git diff
    pub async fn generate_commit_message(
        &self,
        diff: &str,
        format: crate::config::CommitFormat,
    ) -> Result<GeneratedCommit> {
        let system_prompt = match format {
            crate::config::CommitFormat::Conventional => {
                CONVENTIONAL_COMMIT_SYSTEM_PROMPT
            }
            crate::config::CommitFormat::Commitlint => {
                COMMITLINT_SYSTEM_PROMPT
            }
        };

        let prompt = format!("{}", diff);
        let response = self.provider.generate_with_system(system_prompt, &prompt).await?;
        
        self.parse_commit_response(&response, format)
    }

    /// Generate tag message from commits
    pub async fn generate_tag_message(
        &self,
        version: &str,
        commits: &[String],
    ) -> Result<String> {
        let system_prompt = TAG_MESSAGE_SYSTEM_PROMPT;
        let commits_text = commits.join("\n");
        let prompt = format!("Version: {}\n\nCommits:\n{}", version, commits_text);
        
        self.provider.generate_with_system(system_prompt, &prompt).await
    }

    /// Generate changelog entry
    pub async fn generate_changelog_entry(
        &self,
        version: &str,
        commits: &[(String, String)], // (type, message)
    ) -> Result<String> {
        let system_prompt = CHANGELOG_SYSTEM_PROMPT;
        
        let commits_text = commits
            .iter()
            .map(|(t, m)| format!("- [{}] {}", t, m))
            .collect::<Vec<_>>()
            .join("\n");
        
        let prompt = format!("Version: {}\n\nCommits:\n{}", version, commits_text);
        
        self.provider.generate_with_system(system_prompt, &prompt).await
    }

    /// Check if provider is available
    pub async fn is_available(&self) -> bool {
        self.provider.is_available().await
    }

    /// Parse commit response from LLM
    fn parse_commit_response(&self, response: &str, format: crate::config::CommitFormat) -> Result<GeneratedCommit> {
        let lines: Vec<&str> = response.lines().collect();
        
        if lines.is_empty() {
            bail!("Empty response from LLM");
        }

        let first_line = lines[0];
        
        // Parse based on format
        match format {
            crate::config::CommitFormat::Conventional => {
                self.parse_conventional_commit(first_line, lines)
            }
            crate::config::CommitFormat::Commitlint => {
                self.parse_commitlint_commit(first_line, lines)
            }
        }
    }

    fn parse_conventional_commit(
        &self,
        first_line: &str,
        lines: Vec<&str>,
    ) -> Result<GeneratedCommit> {
        // Parse: type(scope)!: description
        let parts: Vec<&str> = first_line.splitn(2, ':').collect();
        if parts.len() != 2 {
            bail!("Invalid conventional commit format: missing colon");
        }

        let type_part = parts[0];
        let description = parts[1].trim();

        // Extract type, scope, and breaking indicator
        let breaking = type_part.ends_with('!');
        let type_part = type_part.trim_end_matches('!');

        let (commit_type, scope) = if let Some(start) = type_part.find('(') {
            if let Some(end) = type_part.find(')') {
                let t = &type_part[..start];
                let s = &type_part[start + 1..end];
                (t.to_string(), Some(s.to_string()))
            } else {
                bail!("Invalid scope format: missing closing parenthesis");
            }
        } else {
            (type_part.to_string(), None)
        };

        // Extract body and footer
        let (body, footer) = self.extract_body_footer(&lines);

        Ok(GeneratedCommit {
            commit_type,
            scope,
            description: description.to_string(),
            body,
            footer,
            breaking,
        })
    }

    fn parse_commitlint_commit(
        &self,
        first_line: &str,
        lines: Vec<&str>,
    ) -> Result<GeneratedCommit> {
        // Similar parsing but with commitlint rules
        let parts: Vec<&str> = first_line.splitn(2, ':').collect();
        if parts.len() != 2 {
            bail!("Invalid commit format: missing colon");
        }

        let type_part = parts[0];
        let subject = parts[1].trim();

        let (commit_type, scope) = if let Some(start) = type_part.find('(') {
            if let Some(end) = type_part.find(')') {
                let t = &type_part[..start];
                let s = &type_part[start + 1..end];
                (t.to_string(), Some(s.to_string()))
            } else {
                (type_part.to_string(), None)
            }
        } else {
            (type_part.to_string(), None)
        };

        let (body, footer) = self.extract_body_footer(&lines);

        Ok(GeneratedCommit {
            commit_type,
            scope,
            description: subject.to_string(),
            body,
            footer,
            breaking: false,
        })
    }

    fn extract_body_footer(&self, lines: &[&str]) -> (Option<String>, Option<String>) {
        if lines.len() <= 1 {
            return (None, None);
        }

        let rest: Vec<&str> = lines[1..]
            .iter()
            .skip_while(|l| l.trim().is_empty())
            .copied()
            .collect();

        if rest.is_empty() {
            return (None, None);
        }

        // Look for footer markers
        let footer_markers = ["BREAKING CHANGE:", "Closes", "Fixes", "Refs", "Co-authored-by:"];
        
        let mut body_lines = vec![];
        let mut footer_lines = vec![];
        let mut in_footer = false;

        for line in &rest {
            if footer_markers.iter().any(|m| line.starts_with(m)) {
                in_footer = true;
            }
            
            if in_footer {
                footer_lines.push(*line);
            } else {
                body_lines.push(*line);
            }
        }

        let body = if body_lines.is_empty() {
            None
        } else {
            Some(body_lines.join("\n"))
        };

        let footer = if footer_lines.is_empty() {
            None
        } else {
            Some(footer_lines.join("\n"))
        };

        (body, footer)
    }
}

/// Generated commit structure
#[derive(Debug, Clone)]
pub struct GeneratedCommit {
    pub commit_type: String,
    pub scope: Option<String>,
    pub description: String,
    pub body: Option<String>,
    pub footer: Option<String>,
    pub breaking: bool,
}

impl GeneratedCommit {
    /// Format as conventional commit
    pub fn to_conventional(&self) -> String {
        crate::utils::formatter::format_conventional_commit(
            &self.commit_type,
            self.scope.as_deref(),
            &self.description,
            self.body.as_deref(),
            self.footer.as_deref(),
            self.breaking,
        )
    }

    /// Format as commitlint commit
    pub fn to_commitlint(&self) -> String {
        crate::utils::formatter::format_commitlint_commit(
            &self.commit_type,
            self.scope.as_deref(),
            &self.description,
            self.body.as_deref(),
            self.footer.as_deref(),
            None,
        )
    }
}

// System prompts for LLM

const CONVENTIONAL_COMMIT_SYSTEM_PROMPT: &str = r#"You are a helpful assistant that generates conventional commit messages.

Analyze the git diff provided and generate a commit message following the Conventional Commits specification.

Format: <type>[optional scope]: <description>

Types:
- feat: A new feature
- fix: A bug fix
- docs: Documentation only changes
- style: Changes that don't affect code meaning (formatting, semicolons, etc.)
- refactor: Code change that neither fixes a bug nor adds a feature
- perf: Code change that improves performance
- test: Adding or correcting tests
- build: Changes to build system or dependencies
- ci: Changes to CI configuration
- chore: Other changes that don't modify src or test files
- revert: Reverts a previous commit

Rules:
1. Use lowercase for type and scope
2. Keep description under 100 characters
3. Use imperative mood ("add" not "added")
4. Don't capitalize first letter
5. No period at the end
6. Include scope if the change is specific to a module/component

Output ONLY the commit message, nothing else.
"#;

const COMMITLINT_SYSTEM_PROMPT: &str = r#"You are a helpful assistant that generates commit messages following @commitlint/config-conventional.

Analyze the git diff and generate a commit message.

Format: <type>[optional scope]: <subject>

Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert

Rules:
1. Subject should not start with uppercase
2. Subject should not end with period
3. Subject should be 4-100 characters
4. Use imperative mood
5. Be concise but descriptive

Output ONLY the commit message, nothing else.
"#;

const TAG_MESSAGE_SYSTEM_PROMPT: &str = r#"You are a helpful assistant that generates git tag annotation messages.

Given a version number and a list of commits, generate a concise but informative tag message.

The message should:
1. Start with a brief summary of the release
2. Group changes by type (features, fixes, etc.)
3. Be suitable for a git annotated tag

Format:
<version> Release

Summary of changes...

Changes:
- Feature: description
- Fix: description
...
"#;

const CHANGELOG_SYSTEM_PROMPT: &str = r#"You are a helpful assistant that generates changelog entries.

Given a version and a list of commits, generate a well-formatted changelog section.

Group commits by:
- Features (feat)
- Bug Fixes (fix)
- Documentation (docs)
- Other Changes

Format in markdown with proper headings and bullet points.
"#;

/// HTTP client helper
pub(crate) fn create_http_client(timeout: Duration) -> Result<reqwest::Client> {
    reqwest::Client::builder()
        .timeout(timeout)
        .build()
        .context("Failed to create HTTP client")
}
