import Foundation
import SaplingCore

public final class MacGitProvider: GitProvider, @unchecked Sendable {
    public let displayName = "System Git"

    public init() {}

    public func initRepository(at path: URL) async throws -> GitRepository {
        #if os(macOS)
        try FileManager.default.createDirectory(at: path, withIntermediateDirectories: true)
        try runGit(["init"], in: path)
        return GitRepository(name: path.lastPathComponent, rootURL: path)
        #else
        throw GitProviderError.unsupportedOnCurrentPlatform
        #endif
    }

    public func clone(remoteURL: URL, to destinationURL: URL) async throws -> GitRepository {
        #if os(macOS)
        try runGit(["clone", remoteURL.absoluteString, destinationURL.path], in: destinationURL.deletingLastPathComponent())
        return GitRepository(name: destinationURL.lastPathComponent, rootURL: destinationURL)
        #else
        throw GitProviderError.unsupportedOnCurrentPlatform
        #endif
    }

    public func repository(at path: URL) -> any Repository {
        MacGitRepository(rootURL: path) { [self] arguments, directory in
            try runGit(arguments, in: directory)
        }
    }

    #if os(macOS)
    @discardableResult
    private func runGit(_ arguments: [String], in directory: URL) throws -> String {
        let process = Process()
        process.executableURL = URL(fileURLWithPath: "/usr/bin/env")
        process.arguments = ["git"] + arguments
        process.currentDirectoryURL = directory

        let stdout = Pipe()
        let stderr = Pipe()
        process.standardOutput = stdout
        process.standardError = stderr

        try process.run()
        process.waitUntilExit()

        let outputData = stdout.fileHandleForReading.readDataToEndOfFile()
        let errorData = stderr.fileHandleForReading.readDataToEndOfFile()
        let output = String(data: outputData + errorData, encoding: .utf8) ?? ""

        guard process.terminationStatus == 0 else {
            throw GitProviderError.commandFailed(
                command: "git \(arguments.joined(separator: " "))",
                exitCode: process.terminationStatus,
                output: output
            )
        }

        return output
    }
    #else
    private func runGit(_ arguments: [String], in directory: URL) throws -> String {
        throw GitProviderError.unsupportedOnCurrentPlatform
    }
    #endif
}

private struct MacGitRepository: Repository {
    let rootURL: URL
    let runner: @Sendable (_ arguments: [String], _ directory: URL) throws -> String

    func status() async throws -> [GitFileStatus] {
        let output = try runner(["status", "--porcelain"], rootURL)
        return output
            .split(separator: "\n")
            .compactMap(parseStatusLine)
    }

    func add(paths: [String]) async throws {
        _ = try runner(["add"] + paths, rootURL)
    }

    func commit(message: String) async throws -> GitCommit {
        _ = try runner(["commit", "-m", message], rootURL)
        let hash = try runner(["rev-parse", "HEAD"], rootURL).trimmingCharacters(in: .whitespacesAndNewlines)
        return GitCommit(
            id: hash,
            shortHash: String(hash.prefix(7)),
            authorName: "",
            authorEmail: "",
            message: message,
            authoredAt: Date()
        )
    }

    func push(remote: String?, branch: String?) async throws {
        _ = try runner(compactArguments(["push", remote, branch]), rootURL)
    }

    func pull(remote: String?, branch: String?) async throws {
        _ = try runner(compactArguments(["pull", remote, branch]), rootURL)
    }

    func createBranch(named name: String) async throws -> GitBranch {
        _ = try runner(["branch", name], rootURL)
        return GitBranch(name: name)
    }

    func switchBranch(named name: String) async throws -> GitBranch {
        _ = try runner(["switch", name], rootURL)
        return GitBranch(name: name, isCurrent: true)
    }

    func merge(branch name: String) async throws {
        _ = try runner(["merge", name], rootURL)
    }

    func addSubmodule(remoteURL: URL, path: String) async throws -> Subproject {
        _ = try runner(["submodule", "add", remoteURL.absoluteString, path], rootURL)
        return Subproject(
            name: URL(fileURLWithPath: path).lastPathComponent,
            path: path,
            repositoryURL: rootURL.appendingPathComponent(path),
            remoteURL: remoteURL
        )
    }

    func removeSubmodule(path: String) async throws {
        // TODO: Coordinate .gitmodules edits, deinit, and metadata cleanup in a transaction.
        _ = try runner(["submodule", "deinit", "-f", path], rootURL)
        _ = try runner(["rm", "-f", path], rootURL)
    }

    func updateSubmodules(initSubmodules: Bool, recursive: Bool) async throws {
        var arguments = ["submodule", "update"]
        if initSubmodules { arguments.append("--init") }
        if recursive { arguments.append("--recursive") }
        _ = try runner(arguments, rootURL)
    }

    func syncSubmodules(recursive: Bool) async throws {
        var arguments = ["submodule", "sync"]
        if recursive { arguments.append("--recursive") }
        _ = try runner(arguments, rootURL)
    }

    private func parseStatusLine(_ line: Substring) -> GitFileStatus? {
        guard line.count >= 4 else { return nil }
        let code = String(line.prefix(2))
        let path = String(line.dropFirst(3))
        let state: GitFileState

        if code.contains("U") {
            state = .conflicted
        } else if code.contains("A") {
            state = .added
        } else if code.contains("D") {
            state = .deleted
        } else if code.contains("R") {
            state = .renamed
        } else if code == "??" {
            state = .untracked
        } else {
            state = .modified
        }

        return GitFileStatus(path: path, state: state)
    }

    private func compactArguments(_ values: [String?]) -> [String] {
        values.compactMap { value in
            guard let value, !value.isEmpty else { return nil }
            return value
        }
    }
}
