use gotcha_gitea::models;

use crate::{
    activity,
    domain::{
        HistoryCommit, HomeData, IssueDetails, MilestoneDetails, PullDetails, RepositoryData,
    },
};

#[derive(Clone, uniffi::Record)]
pub struct ServerRow {
    pub name: String,
    pub url: String,
}

#[derive(Clone, uniffi::Record)]
pub struct RepositoryRow {
    pub name: String,
    pub owner: String,
    pub description: String,
    pub meta: String,
    pub favorite: bool,
}

#[derive(Clone, uniffi::Record)]
pub struct LabelRow {
    pub name: String,
    pub color: String,
    pub light: bool,
}

#[derive(Clone, uniffi::Record)]
pub struct IssueRow {
    pub number: i64,
    pub title: String,
    pub summary: String,
    pub meta: String,
    pub milestone: String,
    pub labels: Vec<LabelRow>,
}

#[derive(Clone, uniffi::Record)]
pub struct PullRow {
    pub number: i64,
    pub owner: String,
    pub repository: String,
    pub title: String,
    pub summary: String,
    pub meta: String,
}

#[derive(Clone, uniffi::Record)]
pub struct CommentRow {
    pub author: String,
    pub body: String,
    pub meta: String,
}

#[derive(Clone, uniffi::Record)]
pub struct IssuePage {
    pub title: String,
    pub state: String,
    pub meta: String,
    pub milestone: String,
    pub body: String,
    pub comments: Vec<CommentRow>,
}

#[derive(Clone, uniffi::Record)]
pub struct MilestoneRow {
    pub id: i64,
    pub title: String,
    pub description: String,
    pub meta: String,
    pub open_issues: i64,
    pub closed_issues: i64,
}

#[derive(Clone, uniffi::Record)]
pub struct MilestonePage {
    pub milestone: MilestoneRow,
    pub issues: Vec<IssueRow>,
}

#[derive(Clone, uniffi::Record)]
pub struct PullPage {
    pub title: String,
    pub meta: String,
    pub body: String,
    pub files_ref: String,
    pub files: Vec<FileRow>,
    pub comments: Vec<CommentRow>,
}

#[derive(Clone, uniffi::Record)]
pub struct CommitRow {
    pub sha: String,
    pub title: String,
    pub meta: String,
    pub refs: String,
    pub top_lanes: Vec<u32>,
    pub bottom_lanes: Vec<u32>,
    pub node_lane: Option<u32>,
    pub connections: Vec<u32>,
}

#[derive(Clone, uniffi::Record)]
pub struct CommitPage {
    pub branches: Vec<String>,
    pub commits: Vec<CommitRow>,
    pub lane_count: u32,
}

#[derive(Clone, uniffi::Record)]
pub struct FileRow {
    pub path: String,
    pub status: String,
}

#[derive(Clone, uniffi::Record)]
pub struct RepositoryContentRow {
    pub name: String,
    pub path: String,
    pub kind: String,
    pub size: i64,
}

#[derive(Clone, uniffi::Record)]
pub struct DiffLine {
    pub old_number: String,
    pub new_number: String,
    pub text: String,
    pub kind: String,
}

#[derive(Clone, uniffi::Record)]
pub struct DiffPage {
    pub title: String,
    pub columns: u32,
    pub lines: Vec<DiffLine>,
}

#[derive(Clone, uniffi::Record)]
pub struct ActivityRow {
    pub icon: String,
    pub title: String,
    pub detail: String,
    pub meta: String,
    pub target: String,
    pub owner: String,
    pub repository: String,
    pub number: i64,
    pub sha: String,
}

#[derive(Clone, uniffi::Record)]
pub struct HeatCell {
    pub week: u32,
    pub day: u32,
    pub level: u32,
    pub timestamp: i64,
    pub contributions: i64,
}

#[derive(Clone, uniffi::Record)]
pub struct HomePage {
    pub server_name: String,
    pub activities: Vec<ActivityRow>,
    pub heat_cells: Vec<HeatCell>,
    pub contribution_count: i64,
}

pub fn repository_rows(
    repositories: &[RepositoryData],
    is_favorite: impl Fn(&RepositoryData) -> bool,
) -> Vec<RepositoryRow> {
    let mut repositories = repositories.to_vec();
    repositories
        .sort_by_key(|repository| (!is_favorite(repository), repository.name.to_lowercase()));
    repositories
        .into_iter()
        .map(|repository| RepositoryRow {
            favorite: is_favorite(&repository),
            meta: format!(
                "{}  ·  {} open  ·  {}",
                repository.language, repository.open_issues, repository.updated
            ),
            name: repository.name,
            owner: repository.owner,
            description: repository.description,
        })
        .collect()
}

pub fn issue_rows(issues: &[models::Issue]) -> Vec<IssueRow> {
    issues
        .iter()
        .map(|issue| IssueRow {
            number: issue.number.unwrap_or_default(),
            title: issue
                .title
                .clone()
                .unwrap_or_else(|| "Untitled issue".into()),
            summary: issue
                .body
                .as_deref()
                .map(summary)
                .unwrap_or_else(|| "No description".into()),
            meta: issue_meta(issue),
            milestone: issue_milestone(issue),
            labels: issue
                .labels
                .as_deref()
                .unwrap_or_default()
                .iter()
                .filter_map(label_row)
                .collect(),
        })
        .collect()
}

pub fn milestone_rows(milestones: &[models::Milestone]) -> Vec<MilestoneRow> {
    let mut milestones = milestones.to_vec();
    milestones.sort_by_key(|milestone| {
        (
            milestone.state.as_deref() == Some("closed"),
            milestone
                .title
                .as_deref()
                .unwrap_or_default()
                .to_lowercase(),
        )
    });
    milestones.iter().map(milestone_row).collect()
}

pub fn milestone_page(details: MilestoneDetails) -> MilestonePage {
    MilestonePage {
        milestone: milestone_row(&details.milestone),
        issues: issue_rows(&details.issues),
    }
}

pub fn pull_rows(pulls: &[models::Issue]) -> Vec<PullRow> {
    pulls
        .iter()
        .filter_map(|pull| {
            let repository = pull.repository.as_ref()?;
            let author = pull
                .user
                .as_ref()
                .and_then(|user| user.login.as_deref())
                .unwrap_or("unknown");
            let kind = if pull
                .pull_request
                .as_ref()
                .and_then(|pull| pull.draft)
                .unwrap_or(false)
            {
                "Draft"
            } else {
                pull.state.as_deref().unwrap_or("unknown")
            };
            Some(PullRow {
                number: pull.number.unwrap_or_default(),
                owner: repository.owner.clone()?,
                repository: repository.name.clone()?,
                title: pull
                    .title
                    .clone()
                    .unwrap_or_else(|| "Untitled pull request".into()),
                summary: pull
                    .body
                    .as_deref()
                    .map(summary)
                    .filter(|body| !body.is_empty())
                    .unwrap_or_else(|| "No description".into()),
                meta: format!(
                    "{kind}  ·  {author}  ·  {}  ·  {} comments",
                    compact_date(pull.updated_at.as_deref()),
                    pull.comments.unwrap_or_default()
                ),
            })
        })
        .collect()
}

pub fn commit_page(
    branches: Vec<String>,
    commits: &[HistoryCommit],
    show_graph: bool,
) -> CommitPage {
    let lane_count = if show_graph {
        commits
            .iter()
            .flat_map(|commit| {
                commit
                    .top_lanes
                    .iter()
                    .chain(commit.bottom_lanes.iter())
                    .chain(commit.connections.iter())
                    .chain(commit.node_lane.iter())
            })
            .max()
            .map_or(0, |lane| lane + 1)
    } else {
        0
    };
    let commits = commits
        .iter()
        .filter_map(|row| {
            let commit = &row.commit;
            let sha = commit.sha.as_deref()?;
            let details = commit.commit.as_ref();
            let author = details
                .and_then(|commit| commit.author.as_ref())
                .and_then(|author| author.name.as_deref())
                .or_else(|| {
                    commit
                        .author
                        .as_ref()
                        .and_then(|author| author.login.as_deref())
                })
                .unwrap_or("unknown");
            let date = details
                .and_then(|commit| commit.author.as_ref())
                .and_then(|author| author.date.as_deref())
                .or(commit.created.as_deref());
            Some(CommitRow {
                sha: sha.into(),
                title: details
                    .and_then(|commit| commit.message.as_deref())
                    .map(|message| message.lines().next().unwrap_or(message))
                    .unwrap_or("Commit")
                    .into(),
                meta: format!("{author}  ·  {}", compact_date(date)),
                refs: row.refs.join("  ·  "),
                top_lanes: indices(&row.top_lanes),
                bottom_lanes: indices(&row.bottom_lanes),
                node_lane: row.node_lane.map(|lane| lane as u32),
                connections: indices(&row.connections),
            })
        })
        .collect();
    CommitPage {
        branches,
        commits,
        lane_count: lane_count as u32,
    }
}

pub fn issue_page(details: IssueDetails) -> IssuePage {
    IssuePage {
        title: details
            .issue
            .title
            .clone()
            .unwrap_or_else(|| "Untitled issue".into()),
        state: details
            .issue
            .state
            .clone()
            .unwrap_or_else(|| "unknown".into()),
        meta: issue_meta(&details.issue),
        milestone: issue_milestone(&details.issue),
        body: details
            .issue
            .body
            .filter(|body| !body.is_empty())
            .unwrap_or_else(|| "No description provided.".into()),
        comments: details.comments.iter().map(comment_row).collect(),
    }
}

pub fn pull_page(details: PullDetails) -> PullPage {
    let pull = &details.pull;
    let author = pull
        .user
        .as_ref()
        .and_then(|user| user.login.as_deref())
        .unwrap_or("unknown");
    let head = pull
        .head
        .as_ref()
        .and_then(|branch| branch.label.as_deref().or(branch.r#ref.as_deref()))
        .unwrap_or("head");
    let base = pull
        .base
        .as_ref()
        .and_then(|branch| branch.label.as_deref().or(branch.r#ref.as_deref()))
        .unwrap_or("base");
    let state = if pull.merged.unwrap_or(false) {
        "merged"
    } else if pull.draft.unwrap_or(false) {
        "draft"
    } else {
        pull.state.as_deref().unwrap_or("unknown")
    };
    PullPage {
        title: pull
            .title
            .clone()
            .unwrap_or_else(|| "Untitled pull request".into()),
        meta: format!(
            "#{}  ·  {state}  ·  {author}  ·  {head} → {base}\n{} files  ·  +{} −{}  ·  {} comments",
            pull.number.unwrap_or_default(),
            pull.changed_files.unwrap_or_default(),
            pull.additions.unwrap_or_default(),
            pull.deletions.unwrap_or_default(),
            pull.comments.unwrap_or_default(),
        ),
        body: pull
            .body
            .clone()
            .filter(|body| !body.is_empty())
            .unwrap_or_else(|| "No description provided.".into()),
        files_ref: pull_files_ref(pull),
        files: details.files.iter().filter_map(file_row).collect(),
        comments: details.comments.iter().map(comment_row).collect(),
    }
}

pub fn file_rows(files: Vec<models::ChangedFile>) -> Vec<FileRow> {
    files.iter().filter_map(file_row).collect()
}

pub fn commit_file_rows(files: Vec<models::CommitAffectedFiles>) -> Vec<FileRow> {
    files
        .into_iter()
        .filter_map(|file| {
            Some(FileRow {
                path: file.filename?,
                status: file.status.unwrap_or_else(|| "modified".into()),
            })
        })
        .collect()
}

pub fn repository_content_rows(
    contents: Vec<models::ContentsResponse>,
) -> Vec<RepositoryContentRow> {
    let mut rows: Vec<_> = contents
        .into_iter()
        .filter_map(|content| {
            Some(RepositoryContentRow {
                name: content.name?,
                path: content.path?,
                kind: content.r#type.unwrap_or_else(|| "file".into()),
                size: content.size.unwrap_or_default(),
            })
        })
        .collect();
    rows.sort_by_key(|row| (row.kind != "dir", row.name.to_lowercase()));
    rows
}

pub fn diff_page(path: &str, diff: crate::diff::Parsed) -> DiffPage {
    DiffPage {
        title: path.rsplit('/').next().unwrap_or(path).into(),
        columns: diff.columns.min(u32::MAX as usize) as u32,
        lines: diff
            .lines
            .into_iter()
            .map(|line| DiffLine {
                old_number: line.old_number,
                new_number: line.new_number,
                text: line.text,
                kind: line.kind.into(),
            })
            .collect(),
    }
}

pub fn home_page(server_name: String, home: HomeData) -> HomePage {
    let (heat_cells, contribution_count) = heat_cells(&home.heatmap);
    HomePage {
        server_name,
        activities: home.activities.iter().map(activity_row).collect(),
        heat_cells,
        contribution_count,
    }
}

fn activity_row(activity: &models::Activity) -> ActivityRow {
    use models::activity::OpType;

    let repository = activity
        .repo
        .as_ref()
        .and_then(|repo| repo.full_name.as_deref())
        .unwrap_or("repository");
    let branch = activity
        .ref_name
        .as_deref()
        .and_then(|name| name.strip_prefix("refs/heads/").or(Some(name)))
        .unwrap_or("default branch");
    let (icon, title) = match activity.op_type {
        Some(OpType::CommitRepo) if activity.content.as_deref().unwrap_or("").is_empty() => (
            "branch-create",
            format!("Created branch {branch} in {repository}"),
        ),
        Some(OpType::CommitRepo | OpType::MirrorSyncPush) => {
            ("push", format!("Pushed to {branch} in {repository}"))
        }
        Some(OpType::CreateRepo) => ("repo-create", format!("Created {repository}")),
        Some(OpType::RenameRepo) => ("repo-rename", format!("Renamed {repository}")),
        Some(OpType::StarRepo) => ("repo-star", format!("Starred {repository}")),
        Some(OpType::WatchRepo) => ("repo-watch", format!("Started watching {repository}")),
        Some(OpType::CreateIssue) => ("issue-open", format!("Opened an issue in {repository}")),
        Some(OpType::CloseIssue) => ("issue-close", format!("Closed an issue in {repository}")),
        Some(OpType::ReopenIssue) => ("issue-reopen", format!("Reopened an issue in {repository}")),
        Some(OpType::CommentIssue) => (
            "issue-comment",
            format!("Commented on an issue in {repository}"),
        ),
        Some(OpType::CreatePullRequest) => (
            "pull-open",
            format!("Opened a pull request in {repository}"),
        ),
        Some(OpType::MergePullRequest | OpType::AutoMergePullRequest) => (
            "pull-merge",
            format!("Merged a pull request in {repository}"),
        ),
        Some(OpType::ClosePullRequest) => (
            "pull-close",
            format!("Closed a pull request in {repository}"),
        ),
        Some(OpType::ReopenPullRequest) => (
            "pull-reopen",
            format!("Reopened a pull request in {repository}"),
        ),
        Some(OpType::CommentPull) => (
            "pull-comment",
            format!("Commented on a pull request in {repository}"),
        ),
        Some(OpType::ApprovePullRequest) => (
            "pull-approve",
            format!("Approved a pull request in {repository}"),
        ),
        Some(OpType::RejectPullRequest) => {
            ("pull-reject", format!("Requested changes in {repository}"))
        }
        Some(OpType::PushTag) => ("tag-push", format!("Pushed tag {branch} in {repository}")),
        Some(OpType::DeleteTag) => (
            "tag-delete",
            format!("Deleted tag {branch} in {repository}"),
        ),
        Some(OpType::DeleteBranch) => (
            "branch-delete",
            format!("Deleted branch {branch} in {repository}"),
        ),
        Some(OpType::PublishRelease) => ("release", format!("Published a release in {repository}")),
        Some(_) | None => ("update", format!("Updated {repository}")),
    };
    let target = activity::target(activity);
    let (target, owner, repository, number, sha) = match target {
        Some(activity::Target::Repository { owner, repository }) => {
            ("repository", owner, repository, 0, String::new())
        }
        Some(activity::Target::Issue {
            owner,
            repository,
            number,
        }) => ("issue", owner, repository, number, String::new()),
        Some(activity::Target::Pull {
            owner,
            repository,
            number,
        }) => ("pull", owner, repository, number, String::new()),
        Some(activity::Target::Commit {
            owner,
            repository,
            sha,
        }) => ("commit", owner, repository, 0, sha),
        None => ("", String::new(), String::new(), 0, String::new()),
    };
    ActivityRow {
        icon: icon.into(),
        title,
        detail: activity_detail(activity),
        meta: compact_date(activity.created.as_deref()),
        target: target.into(),
        owner,
        repository,
        number,
        sha,
    }
}

fn activity_detail(activity: &models::Activity) -> String {
    let text = activity
        .comment
        .as_ref()
        .and_then(|comment| comment.body.as_deref())
        .or(activity.content.as_deref())
        .unwrap_or("");
    if let Ok(payload) = serde_json::from_str::<serde_json::Value>(text) {
        let commits = payload.get("Len").and_then(|value| value.as_i64());
        let message = payload
            .get("HeadCommit")
            .and_then(|commit| commit.get("Message"))
            .and_then(|message| message.as_str())
            .map(summary)
            .unwrap_or_default();
        if !message.is_empty() {
            return match commits {
                Some(1) => format!("1 commit · {message}"),
                Some(count) => format!("{count} commits · {message}"),
                None => message,
            };
        }
    }
    let text = summary(text);
    if text.is_empty() {
        "Server activity".into()
    } else {
        text
    }
}

fn heat_cells(data: &[models::UserHeatmapData]) -> (Vec<HeatCell>, i64) {
    const DAYS: i64 = 9 * 31;
    let latest = data
        .iter()
        .filter_map(|entry| entry.timestamp)
        .max()
        .unwrap_or(0)
        / 86_400;
    let start = latest - DAYS + 1;
    let mut counts = vec![0_i64; DAYS as usize];
    for entry in data {
        let day = entry.timestamp.unwrap_or_default() / 86_400;
        if (start..=latest).contains(&day) {
            counts[(day - start) as usize] += entry.contributions.unwrap_or_default();
        }
    }
    let maximum = counts.iter().copied().max().unwrap_or_default();
    let contribution_count = counts.iter().sum();
    let cells = counts
        .into_iter()
        .enumerate()
        .map(|(index, count)| HeatCell {
            week: (index / 7) as u32,
            day: (index % 7) as u32,
            level: if count == 0 || maximum == 0 {
                0
            } else {
                ((count * 4 + maximum - 1) / maximum).clamp(1, 4) as u32
            },
            timestamp: (start + index as i64) * 86_400,
            contributions: count,
        })
        .collect();
    (cells, contribution_count)
}

fn label_row(label: &models::Label) -> Option<LabelRow> {
    let name = label.name.as_deref()?.trim();
    let color = label.color.as_deref()?.trim().trim_start_matches('#');
    if name.is_empty() || color.len() != 6 {
        return None;
    }
    let value = u32::from_str_radix(color, 16).ok()?;
    let red = (value >> 16) & 0xff;
    let green = (value >> 8) & 0xff;
    let blue = value & 0xff;
    Some(LabelRow {
        name: name.into(),
        color: format!("#{color}"),
        light: red * 299 + green * 587 + blue * 114 > 150_000,
    })
}

fn comment_row(comment: &models::Comment) -> CommentRow {
    CommentRow {
        author: comment
            .user
            .as_ref()
            .and_then(|user| user.login.as_deref())
            .or(comment.original_author.as_deref())
            .unwrap_or("unknown")
            .into(),
        body: comment
            .body
            .clone()
            .filter(|body| !body.is_empty())
            .unwrap_or_else(|| "No comment text.".into()),
        meta: compact_date(
            comment
                .updated_at
                .as_deref()
                .or(comment.created_at.as_deref()),
        ),
    }
}

fn file_row(file: &models::ChangedFile) -> Option<FileRow> {
    Some(FileRow {
        path: file.filename.clone()?,
        status: file.status.clone().unwrap_or_else(|| "modified".into()),
    })
}

fn pull_files_ref(pull: &models::PullRequest) -> String {
    if pull.state.as_deref() == Some("open") {
        let branch = pull
            .head
            .as_ref()
            .and_then(|head| head.r#ref.as_deref().or(head.label.as_deref()))
            .unwrap_or("head branch");
        format!("Files on {branch}")
    } else if let Some(sha) = pull.merge_commit_sha.as_deref() {
        format!("Files at merge commit {}", &sha[..sha.len().min(8)])
    } else {
        "Files from pull request".into()
    }
}

fn issue_meta(issue: &models::Issue) -> String {
    let author = issue
        .user
        .as_ref()
        .and_then(|user| user.login.as_deref())
        .unwrap_or("unknown");
    format!(
        "#{}  ·  {}  ·  updated {}  ·  {} comments",
        issue.number.unwrap_or_default(),
        author,
        compact_date(issue.updated_at.as_deref()),
        issue.comments.unwrap_or_default()
    )
}

fn issue_milestone(issue: &models::Issue) -> String {
    issue
        .milestone
        .as_ref()
        .and_then(|milestone| milestone.title.as_deref())
        .unwrap_or_default()
        .into()
}

fn milestone_row(milestone: &models::Milestone) -> MilestoneRow {
    let open = milestone.open_issues.unwrap_or_default();
    let closed = milestone.closed_issues.unwrap_or_default();
    let due = milestone
        .due_on
        .as_deref()
        .map(|date| format!("due {}", compact_date(Some(date))))
        .unwrap_or_else(|| "no due date".into());
    MilestoneRow {
        id: milestone.id.unwrap_or_default(),
        title: milestone
            .title
            .clone()
            .unwrap_or_else(|| "Untitled milestone".into()),
        description: milestone
            .description
            .clone()
            .filter(|description| !description.is_empty())
            .unwrap_or_else(|| "No description".into()),
        meta: format!(
            "{}  ·  {closed} of {} closed  ·  {due}",
            milestone.state.as_deref().unwrap_or("unknown"),
            open + closed
        ),
        open_issues: open,
        closed_issues: closed,
    }
}

fn summary(body: &str) -> String {
    body.split_whitespace()
        .take(24)
        .collect::<Vec<_>>()
        .join(" ")
}

fn indices(values: &[usize]) -> Vec<u32> {
    values.iter().map(|value| *value as u32).collect()
}

pub fn compact_date(date: Option<&str>) -> String {
    date.and_then(|date| date.get(..10))
        .unwrap_or("unknown")
        .to_string()
}

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

    #[test]
    fn repository_directories_sort_before_files() {
        let contents = [
            ("z.swift", "file"),
            ("Sources", "dir"),
            ("README.md", "file"),
        ]
        .into_iter()
        .map(|(name, kind)| models::ContentsResponse {
            name: Some(name.into()),
            path: Some(name.into()),
            r#type: Some(kind.into()),
            ..Default::default()
        })
        .collect();

        assert_eq!(
            repository_content_rows(contents)
                .into_iter()
                .map(|row| row.name)
                .collect::<Vec<_>>(),
            ["Sources", "README.md", "z.swift"]
        );
    }

    #[test]
    fn maps_heat_levels_and_labels() {
        let heatmap = vec![
            models::UserHeatmapData {
                timestamp: Some(100 * 86_400),
                contributions: Some(1),
            },
            models::UserHeatmapData {
                timestamp: Some(101 * 86_400),
                contributions: Some(4),
            },
        ];
        let (cells, total) = heat_cells(&heatmap);
        assert_eq!((cells.len(), total), (279, 5));
        assert_eq!(
            (
                cells[277].level,
                cells[277].timestamp,
                cells[277].contributions,
                cells[278].level,
            ),
            (1, 100 * 86_400, 1, 4)
        );

        let label = models::Label {
            name: Some("bug".into()),
            color: Some("d73a4a".into()),
            ..Default::default()
        };
        assert!(label_row(&label).is_some());
        assert!(label_row(&models::Label::default()).is_none());

        let milestone = models::Milestone {
            id: Some(1),
            title: Some("Version 1".into()),
            open_issues: Some(3),
            closed_issues: Some(2),
            state: Some("open".into()),
            ..Default::default()
        };
        let milestone_row = milestone_rows(std::slice::from_ref(&milestone)).remove(0);
        assert_eq!(
            (milestone_row.open_issues, milestone_row.closed_issues),
            (3, 2)
        );
        let issue = models::Issue {
            milestone: Some(Box::new(milestone)),
            ..Default::default()
        };
        assert_eq!(issue_rows(&[issue])[0].milestone, "Version 1");
    }

    #[test]
    fn issue_page_exposes_state() {
        let page = issue_page(IssueDetails {
            issue: models::Issue {
                state: Some("closed".into()),
                ..Default::default()
            },
            comments: Vec::new(),
        });

        assert_eq!(page.state, "closed");
    }
}
