import { Octokit, type App } from "octokit"
import * as Sentry from "@sentry/node"
import {
  createAppInstallation,
  createOrUpdateUser,
  deleteOrganizationMemberApiKeys,
  getAppInstallationByInstalltionId,
  getUserById,
  organizationMemberRepository,
  organizationRepository,
  prisma,
  upsertRepository,
} from "@codeflash-ai/common"
import { sendSlackMessage } from "./slack_util.js"
import { isCodeflashEmployee } from "../utils/employee-utils.js"

// Channel for GitHub App installation alerts (falls back to default channel)
const GITHUB_ALERTS_CHANNEL = process.env.SLACK_CHANNEL_ID

// Dependencies interface for easier testing
export interface GithubUtilsDependencies {
  console: {
    error: typeof console.error
  }
}

// Default dependencies
let dependencies: GithubUtilsDependencies = {
  console: {
    error: console.error,
  },
}

// For testing - allow dependency injection
export function setGithubUtilsDependencies(deps: Partial<GithubUtilsDependencies>) {
  dependencies = { ...dependencies, ...deps }
}

export function resetGithubUtilsDependencies() {
  dependencies = {
    console: {
      error: console.error,
    },
  }
}

// Checks if the user is a collaborator on the repository
export async function isUserCollaborator(
  installationOctokit: Octokit,
  owner: string,
  repo: string,
  nickname: string,
): Promise<boolean> {
  try {
    const response = await installationOctokit.rest.repos.checkCollaborator({
      owner,
      repo,
      username: nickname,
    })
    // If the request is successful, the user is a collaborator and we can continue
    return response.status === 204
  } catch (error: any) {
    if (error.status === 404) {
      return false // User is not a collaborator
    }
    // Handle other potential errors
    throw error // Rethrow error for upstream handling
  }
}

export async function getInstallationOctokitByOwner(
  githubApp: App,
  owner: string,
  repo: string,
  userId?: string,
): Promise<Octokit | Error> {
  try {
    const repoInstallation = await githubApp.octokit.rest.apps.getRepoInstallation({
      owner,
      repo,
    })
    const installationId = repoInstallation.data.id
    return await githubApp.getInstallationOctokit(installationId)
  } catch (error: any) {
    dependencies.console.error(`Error getting installation Octokit for ${owner}/${repo}:`, error)
    if (error.status === 404) {
      // Send Slack alert for GitHub App installation failures
      // This helps the team proactively reach out to affected customers
      // Skip notification for Codeflash employees to reduce internal noise
      if (!userId || !isCodeflashEmployee(userId)) {
        sendSlackMessage(
          {
            text: `GitHub App Installation Issue`,
            blocks: [
              {
                type: "header",
                text: {
                  type: "plain_text",
                  text: "GitHub App Installation Issue",
                  emoji: true,
                },
              },
              {
                type: "section",
                fields: [
                  {
                    type: "mrkdwn",
                    text: `*Repository:*\n${owner}/${repo}`,
                  },
                  {
                    type: "mrkdwn",
                    text: `*Error:*\nApp not installed (404)`,
                  },
                ],
              },
              {
                type: "section",
                text: {
                  type: "mrkdwn",
                  text: `This may indicate the customer needs to accept updated GitHub App permissions or reinstall the app.\n\n<https://github.com/${owner}|View Organization on GitHub>`,
                },
              },
              {
                type: "context",
                elements: [
                  {
                    type: "mrkdwn",
                    text: `Timestamp: ${new Date().toISOString()}`,
                  },
                ],
              },
            ],
          },
          GITHUB_ALERTS_CHANNEL,
        ).catch(slackError => {
          // Don't let Slack errors affect the main flow
          console.error("Failed to send Slack alert for GitHub App installation issue:", slackError)
        })
      }

      return Error(`GitHub App is not installed on the repository ${owner}/${repo}`)
    } else {
      return Error(`Error checking GitHub App installation status for repo ${owner}/${repo}`)
    }
  }
}

function isLabelAlreadyExistsError(error: any): boolean {
  return (
    error?.status === 422 &&
    Array.isArray(error?.response?.data?.errors) &&
    error.response.data.errors.some(
      (e: any) => e.resource === "Label" && e.code === "already_exists" && e.field === "name",
    )
  )
}

// Ensures that a label exists on a repository, creating it if it doesn't
export async function ensureLabelExists(
  installationOctokit: Octokit,
  owner: string,
  repo: string,
  labelName: string,
  color: string,
  description: string,
): Promise<void> {
  try {
    await installationOctokit.rest.issues.getLabel({
      owner,
      repo,
      name: labelName,
    })
  } catch (error: any) {
    if (error.status === 404) {
      // Label does not exist, create it
      try {
        await installationOctokit.rest.issues.createLabel({
          owner,
          repo,
          name: labelName,
          color,
          description,
        })
      } catch (createError: any) {
        // Ignore "already_exists" error - another concurrent request may have created it
        if (!isLabelAlreadyExistsError(createError)) {
          Sentry.captureException(createError, {
            extra: { owner, repo, labelName, context: "ensureLabelExists - createLabel failed" },
          })
        }
      }
    } else {
      // Log to Sentry and prevent throwing the error
      Sentry.captureException(error, {
        extra: { owner, repo, labelName, context: "ensureLabelExists - getLabel failed" },
      })
    }
  }
}

// Adds a label to a pull request, creating the label if it doesn't exist
export async function addLabelToPullRequest(
  installationOctokit: Octokit,
  owner: string,
  repo: string,
  pullNumber: number,
  labelName: string = "⚡️ codeflash",
  labelColor: string = "FFC043",
  description: string = "Optimization PR opened by Codeflash AI",
): Promise<void> {
  await ensureLabelExists(installationOctokit, owner, repo, labelName, labelColor, description)

  await installationOctokit.rest.issues.addLabels({
    owner,
    repo,
    issue_number: pullNumber,
    labels: [labelName],
  })
}

/**
 * Fetches the user's role in an organization.
 * Expected values: "admin" (owner), "member" (normal member).
 */
async function fetchOrgRole(octokit: Octokit, owner: string, username: string): Promise<string> {
  try {
    const { data: membership } = await octokit.rest.orgs.getMembershipForUser({
      org: owner,
      username,
    })
    return membership.role || ""
  } catch (error) {
    console.error(`Error fetching org role for ${username} in ${owner}:`, error)
    Sentry.captureException(error)
    return ""
  }
}

/**
 * Fetches the user's permission level in a repository.
 * Expected values: "admin", "maintain", "write", "triage", "read".
 */
async function fetchRepoRole(
  octokit: Octokit,
  owner: string,
  repo: string,
  username: string,
): Promise<string> {
  try {
    const { data: permissions } = await octokit.rest.repos.getCollaboratorPermissionLevel({
      owner,
      repo,
      username,
    })
    console.log(`Repo role for ${username} in ${owner}/${repo}: ${permissions.permission}`)
    return permissions.permission || ""
  } catch (error) {
    console.error(`Error fetching repo role for ${username} in ${owner}/${repo}:`, error)
    Sentry.captureException(error)
    return ""
  }
}

/**
 * Returns the user's role in an org or a repo, depending on what's available.
 * If it fails, logs the error to Sentry and returns an empty string.
 */
export async function getUserRole({
  octokit,
  owner,
  repo,
  username,
  isOrg,
}: {
  octokit: Octokit
  owner: string
  repo: string
  username: string
  isOrg: boolean
}): Promise<string> {
  if (isOrg) {
    // Fetch org membership role
    // Expected values: "admin" (org owner) or "member" (regular member)
    return await fetchOrgRole(octokit, owner, username)
  } else {
    // Fetch repo permission
    // Expected values: "admin", "maintain", "write", "triage", "read"
    return await fetchRepoRole(octokit, owner, repo, username)
  }
}

async function getInstallations(app: App) {
  const installations: any[] = []
  let page = 1

  console.log("fetching installations...")

  while (true) {
    try {
      const response = await app.octokit.rest.apps.listInstallations({
        per_page: 90, // max is 90
        page,
      })

      if (!response.data.length) break

      installations.push(...response.data)
      page++
    } catch (error) {
      console.error("Error fetching installations:", error)
      Sentry.captureException(error)
      break
    }
  }

  console.log("Done")
  return installations
}
/**
 * Checks whether a branch exists in a GitHub repository.
 *
 * @param {object} params - Parameters for branch existence check.
 * @param {string} params.owner - The repository owner.
 * @param {string} params.repo - The repository name.
 * @param {string} params.branch - The branch name to check.
 * @param {Octokit} params.installationOctokit - The authenticated installation Octokit client for the repo.
 * @returns {Promise<boolean>} - Returns true if the branch exists, false if not.
 *
 * @example
 * const exists = await isBranchExists({
 *   owner: "octocat",
 *   repo: "Hello-World",
 *   branch: "feature-branch",
 *   installationOctokit,
 * });
 * console.log("Branch exists:", exists);
 */
export async function isBranchExists({
  owner,
  repo,
  branch,
  installationOctokit,
}: {
  owner: string
  repo: string
  branch: string
  installationOctokit: Octokit
}): Promise<boolean> {
  try {
    await installationOctokit.rest.git.getRef({
      owner,
      repo,
      ref: `heads/${branch}`,
    })
    return true
  } catch (error: any) {
    if (error.status === 404) {
      return false
    }
    Sentry.captureException(error)
    dependencies.console.error(
      `Error checking if branch ${branch} exists in ${owner}/${repo}:`,
      error,
    )
    return false
  }
}

async function filterInstalltionAccountByOrg(installations: any, orgNames?: string[]) {
  let orgInstallations: typeof installations = []
  for (const installation of installations) {
    const login = installation.account?.login
    if (installation.account?.type === "Organization" && login) {
      orgInstallations.push(installation)
    }
  }
  if (orgNames && orgNames.length) {
    orgInstallations = orgInstallations.filter(install => orgNames.includes(install.account!.login))
  }
  return orgInstallations
}

async function getReposForInstallation(installationOctokit: Octokit): Promise<any[]> {
  const repos: any[] = []
  let page = 1

  while (true) {
    try {
      const reposResponse = await installationOctokit.rest.apps.listReposAccessibleToInstallation({
        per_page: 90,
        page,
      })

      if (!reposResponse.data.repositories.length) break

      repos.push(...reposResponse.data.repositories)
      page++
    } catch (error) {
      console.error("Error fetching repos for installation:", error)
      Sentry.captureException(error)
      break
    }
  }

  return repos
}

async function getMembersWithRolesForOrg(
  installationOctokit: Octokit,
  orgLogin: string,
): Promise<Array<{ id: number; username: string; role: string }>> {
  const members: Array<{ id: number; username: string; role: string }> = []
  const memberData: Array<{ id: number; login: string }> = []
  let page = 1

  // ---- Fetch members (paginated) ----
  while (true) {
    try {
      const membersResponse = await installationOctokit.rest.orgs.listMembers({
        org: orgLogin,
        per_page: 90,
        page,
      })

      if (!membersResponse.data.length) break

      memberData.push(
        ...membersResponse.data.map(member => ({
          id: member.id,
          login: member.login,
        })),
      )

      page++
    } catch (error) {
      console.error(`Error fetching members for org ${orgLogin}:`, error)
      Sentry.captureException(error)
      break
    }
  }

  // ---- Fetch roles for each member ----
  for (const member of memberData) {
    try {
      const role = await fetchOrgRole(installationOctokit, orgLogin, member.login)
      members.push({ id: member.id, username: member.login, role })
    } catch (error) {
      console.error(`Error fetching role for member ${member.login} in org ${orgLogin}:`, error)
      Sentry.captureException(error)
    }
  }

  return members
}

export async function syncOrgsWithMembers(app: App, orgNames?: string[]) {
  Sentry.captureMessage("syncOrgsWithMembers started")
  const installations = await getInstallations(app)
  const orgInstallations = await filterInstalltionAccountByOrg(installations, orgNames)

  for (const installation of orgInstallations) {
    try {
      const login = installation.account!.login
      let repos: any[] = []
      let members: Array<{ id: number; username: string; role: string }> = []
      console.log(`fetch repos for ${login}`)

      const installationOctokit = await app.getInstallationOctokit(installation.id)

      const accountLogin = installation.account!.login
      const accountType = installation.account!.type

      // Check if the installation exists, if not, create it
      const installationExists = await getAppInstallationByInstalltionId(installation.id)

      if (!installationExists) {
        await createAppInstallation({
          installation_id: installation.id,
          account_id: installation.account!.id,
          account_login: accountLogin,
          account_type: accountType,
        })
        console.log(`Installation created for ID: ${installation.id}`)
      }

      // --- Fetch all repos with pagination ---
      repos = await getReposForInstallation(installationOctokit)

      console.log("Done... ")
      console.log(`fetch members for ${login}`)

      // --- Fetch all members with roles ---
      members = await getMembersWithRolesForOrg(installationOctokit, login)

      console.log("Done... ")

      // Get or create organization
      let organization = await organizationRepository.findByGithubOrgId(
        String(installation.account!.id),
      )
      organization ||= await organizationRepository.create({
        github_org_id: String(installation.account!.id),
        name: login,
        added_by: "Codeflash",
      })

      // Fetch existing members in organization from DB
      const existingMembersInDb = await prisma.organization_members.findMany({
        where: { organization_id: organization.id },
      })

      // Current member IDs from GitHub
      const currentMemberIds = members.map(m => `github|${m.id}`)

      // Remove members who no longer exist in the org
      for (const existingMember of existingMembersInDb) {
        if (!currentMemberIds.includes(String(existingMember.user_id))) {
          await organizationMemberRepository.removeMember(
            String(organization.id),
            String(existingMember.user_id),
          )
          await deleteOrganizationMemberApiKeys(
            String(existingMember.user_id),
            String(organization.id),
          )
        }
      }

      // Process each member: create or update user and add/update organization membership
      for (const member of members) {
        const userId = `github|${member.id}`
        const existingUser = await getUserById(userId)
        if (!existingUser) {
          await createOrUpdateUser(userId, member.username, null, null)
        }
        try {
          // Check if member already exists in organization_members
          const existingMember = await prisma.organization_members.findUnique({
            where: {
              organization_id_user_id: {
                organization_id: organization.id,
                user_id: userId,
              },
            },
          })
          if (!existingMember) {
            await organizationMemberRepository.addMember({
              organizationId: organization.id,
              userId,
              role: member.role,
            })
          } else if (existingMember.role !== member.role) {
            // Update role if changed
            await organizationMemberRepository.updateRole(organization.id, userId, member.role)
          }
        } catch (error) {
          // If error is due to unique constraint, skip
          if (error instanceof Error && error.message.includes("Unique constraint failed")) {
            // skip
          } else {
            throw error
          }
        }
      }

      // Process each repo
      for (const repo of repos) {
        // Upsert repository
        await upsertRepository({
          github_repo_id: String(repo.id),
          installation_id: installation.id,
          name: repo.name,
          full_name: repo.full_name,
          is_private: repo.private,
          organization_id: organization.id,
        })
      }
    } catch (error) {
      dependencies.console.error(
        `Error fetching data for org ${installation.account!.login}:`,
        error,
      )
      Sentry.captureException(error)
      continue
    }
  }

  Sentry.captureMessage("syncOrgsWithMembers completed")
}
