use chrono::{DateTime, Utc};
use serde::Deserialize;
use std::path::PathBuf;

use crate::error::{Result, TuicrError};
use crate::forge::traits::{
    ForgeRepository, PullRequestCommit, PullRequestDetails, PullRequestSummary,
};
use crate::model::FileStatus;
use crate::vcs::git::raw::FileMetadata;

/// Machine-readable entry from the pull-request files REST endpoint.
#[derive(Debug, Deserialize)]
pub struct GhPullRequestFile {
    pub filename: String,
    pub status: String,
    #[serde(default)]
    pub previous_filename: Option<String>,
}

#[derive(Debug, Deserialize)]
pub struct GhCompare {
    #[serde(default)]
    pub files: Vec<GhPullRequestFile>,
}

impl GhPullRequestFile {
    pub(crate) fn into_metadata(self) -> Result<FileMetadata> {
        let new_path = PathBuf::from(&self.filename);
        let metadata = match self.status.as_str() {
            "added" => FileMetadata {
                old_path: None,
                new_path: Some(new_path),
                status: FileStatus::Added,
            },
            "removed" => FileMetadata {
                old_path: Some(new_path),
                new_path: None,
                status: FileStatus::Deleted,
            },
            "renamed" => FileMetadata {
                old_path: Some(PathBuf::from(self.previous_filename.ok_or_else(|| {
                    TuicrError::Forge(format!(
                        "GitHub renamed file `{}` has no previous_filename",
                        self.filename
                    ))
                })?)),
                new_path: Some(new_path),
                status: FileStatus::Renamed,
            },
            "copied" => FileMetadata {
                old_path: Some(PathBuf::from(self.previous_filename.ok_or_else(|| {
                    TuicrError::Forge(format!(
                        "GitHub copied file `{}` has no previous_filename",
                        self.filename
                    ))
                })?)),
                new_path: Some(new_path),
                status: FileStatus::Copied,
            },
            "modified" | "changed" | "unchanged" => FileMetadata {
                old_path: Some(new_path.clone()),
                new_path: Some(new_path),
                status: FileStatus::Modified,
            },
            status => {
                return Err(TuicrError::Forge(format!(
                    "GitHub returned unsupported file status `{status}` for `{}`",
                    self.filename
                )));
            }
        };
        Ok(metadata)
    }
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GhPullRequestSummary {
    pub number: u64,
    #[serde(default)]
    pub title: String,
    #[serde(default)]
    pub author: Option<GhAuthor>,
    #[serde(default)]
    pub head_ref_name: String,
    #[serde(default)]
    pub base_ref_name: String,
    #[serde(default)]
    pub updated_at: Option<DateTime<Utc>>,
    #[serde(default)]
    pub url: String,
    #[serde(default)]
    pub state: String,
    #[serde(default)]
    pub is_draft: bool,
}

impl GhPullRequestSummary {
    pub fn into_summary(self, repository: &ForgeRepository) -> PullRequestSummary {
        PullRequestSummary {
            repository: repository.clone(),
            number: self.number,
            title: self.title,
            author: self.author.and_then(|author| author.login),
            head_ref_name: self.head_ref_name,
            base_ref_name: self.base_ref_name,
            updated_at: self.updated_at,
            url: self.url,
            state: self.state,
            is_draft: self.is_draft,
        }
    }
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GhPullRequestDetails {
    pub number: u64,
    #[serde(default)]
    pub title: String,
    #[serde(default)]
    pub url: String,
    #[serde(default)]
    pub state: String,
    #[serde(default)]
    pub is_draft: bool,
    #[serde(default)]
    pub author: Option<GhAuthor>,
    #[serde(default)]
    pub head_ref_name: String,
    #[serde(default)]
    pub base_ref_name: String,
    #[serde(default)]
    pub head_ref_oid: String,
    #[serde(default)]
    pub base_ref_oid: String,
    #[serde(default)]
    pub body: String,
    #[serde(default)]
    pub updated_at: Option<DateTime<Utc>>,
    #[serde(default)]
    pub closed: bool,
    #[serde(default)]
    pub merged_at: Option<DateTime<Utc>>,
}

impl GhPullRequestDetails {
    pub fn into_details(self, repository: &ForgeRepository) -> Result<PullRequestDetails> {
        require_field(&self.head_ref_oid, "headRefOid")?;
        require_field(&self.base_ref_oid, "baseRefOid")?;

        Ok(PullRequestDetails {
            repository: repository.clone(),
            number: self.number,
            title: self.title,
            url: self.url,
            state: self.state,
            is_draft: self.is_draft,
            author: self.author.and_then(|author| author.login),
            head_ref_name: self.head_ref_name,
            base_ref_name: self.base_ref_name,
            head_sha: self.head_ref_oid,
            base_sha: self.base_ref_oid,
            body: self.body,
            updated_at: self.updated_at,
            closed: self.closed,
            merged_at: self.merged_at,
            diff_start_sha: None,
        })
    }
}

#[derive(Debug, Deserialize)]
pub struct GhAuthor {
    #[serde(default)]
    pub login: Option<String>,
}

/// Response shape for `gh api repos/<owner>/<repo>/pulls/<num>/commits`.
/// We only consume a small subset of fields; the rest are ignored.
#[derive(Debug, Deserialize)]
pub struct GhPrCommit {
    pub sha: String,
    #[serde(default)]
    pub commit: GhCommitDetails,
}

#[derive(Debug, Default, Deserialize)]
pub struct GhCommitDetails {
    #[serde(default)]
    pub message: String,
    #[serde(default)]
    pub author: Option<GhCommitAuthor>,
}

#[derive(Debug, Deserialize)]
pub struct GhCommitAuthor {
    #[serde(default)]
    pub name: Option<String>,
    #[serde(default)]
    pub email: Option<String>,
    #[serde(default)]
    pub date: Option<DateTime<Utc>>,
}

impl GhPrCommit {
    pub fn into_pull_request_commit(self) -> PullRequestCommit {
        let summary = self.commit.message.lines().next().unwrap_or("").to_string();
        let (author, timestamp) = match self.commit.author {
            Some(a) => (
                a.name.or(a.email).unwrap_or_else(|| "unknown".to_string()),
                a.date,
            ),
            None => ("unknown".to_string(), None),
        };
        let short_oid = self.sha.chars().take(7).collect();
        PullRequestCommit {
            oid: self.sha,
            short_oid,
            summary,
            author,
            timestamp,
        }
    }
}

fn require_field(value: &str, field: &str) -> Result<()> {
    if value.is_empty() {
        Err(TuicrError::Forge(format!(
            "GitHub response did not include required field `{field}`"
        )))
    } else {
        Ok(())
    }
}

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

    #[test]
    fn file_metadata_uses_json_paths_verbatim() {
        let file: GhPullRequestFile = serde_json::from_str(
            r#"{
                "filename":"新 b/right and side.txt",
                "previous_filename":"旧 b/left and side.txt",
                "status":"renamed"
            }"#,
        )
        .unwrap();
        let metadata = file.into_metadata().unwrap();

        assert_eq!(metadata.status, FileStatus::Renamed);
        assert_eq!(
            metadata.old_path.as_deref(),
            Some(Path::new("旧 b/left and side.txt"))
        );
        assert_eq!(
            metadata.new_path.as_deref(),
            Some(Path::new("新 b/right and side.txt"))
        );
    }

    #[test]
    fn renamed_file_requires_previous_filename() {
        let file: GhPullRequestFile =
            serde_json::from_str(r#"{"filename":"new.txt","status":"renamed"}"#).unwrap();
        assert!(file.into_metadata().is_err());
    }
}
