# -----------------------------------------------------------------------------
# Copyright (C) 2026 Iliyas Jorio.
# This file is part of GitFourchette, distributed under the GNU GPL v3.
# For full terms, see the included LICENSE file.
# -----------------------------------------------------------------------------

from pathlib import Path

from gitfourchette.gitdriver import GitDelta, GitStatus, GitDriver
from gitfourchette.localization import *
from gitfourchette.porcelain import *
from gitfourchette.tasks.repotask import AbortTask, RepoTask, TaskEffects
from gitfourchette.toolbox import *


def savePatch(task: RepoTask, patch: str, fileName="") -> RepoTask.Flow[str]:
    if not patch:
        raise AbortTask(_("Nothing to export. The patch is empty."), icon="information")

    # Sanitize filename
    for c in "?/\\*~<>|:":
        fileName = fileName.replace(c, "_")

    qfd = PersistentFileDialog.saveFile(task.parentWidget(), "SaveFile", task.name(), fileName)
    savePath = yield from task.flowFileDialog(qfd)

    yield from task.flowEnterWorkerThread()
    # Write patches verbatim: force UTF-8 (not the locale encoding, which fails
    # on non-cp1252 content on Windows) and keep LF line endings (CRLF
    # translation corrupts --binary base85 payloads and can break git apply).
    Path(savePath).write_text(patch, encoding="utf-8", newline="\n")

    if task.repo.is_in_workdir(savePath):
        task.epilog.effects |= TaskEffects.Workdir  # invalidate workdir if saved file to it

    task.epilog.status = _("Patch saved as: {0}", tquo(compactPath(savePath)))
    return savePath


class ExportCommitAsPatch(RepoTask):
    def flow(self, oid: Oid, fileName=""):
        commit = self.repo.peel_commit(oid)

        if not fileName:
            summary, _dummy = messageSummary(commit.message, elision="")
            summary = summary[:50].strip()
            fileName = f"{self.repo.repo_name()} - {shortHash(oid)} - {summary}.patch"

        diffAB = commit_diff_pair(commit)
        tokens = GitDriver.buildDiffCommand(diffAB)
        driver = yield from self.flowCallGit(*tokens)
        patch = driver.stdoutScrollback()

        yield from savePatch(self, patch, fileName)


class ExportStashAsPatch(ExportCommitAsPatch):
    def flow(self, oid: Oid, fileName=""):
        if not fileName:
            commit = self.repo.peel_commit(oid)
            message = _p("patch file name, please keep it short",
                         "stashed on {commit}",
                         commit=shortHash(commit.parent_ids[0]))
            summary = strip_stash_message(commit.message)[:50].strip()
            fileName = f"{self.repo.repo_name()} - {message} - {summary}.patch"

        yield from super().flow(oid, fileName)


class ExportABDiffAsPatch(RepoTask):
    def flow(self, diffAB: tuple[Oid, Oid]):
        fileName = f"{self.repo.repo_name()} - {shortHash(diffAB[0])}...{shortHash(diffAB[1])}.patch"

        tokens = GitDriver.buildDiffCommand(diffAB)
        driver = yield from self.flowCallGit(*tokens)
        patch = driver.stdoutScrollback()

        yield from savePatch(self, patch, fileName)


class ExportWorkdirAsPatch(RepoTask):
    def flow(self):
        patches = []

        # Diff the workdir to HEAD (except untracked files)
        tokens = GitDriver.buildDiffCommand(None)
        driver = yield from self.flowCallGit(*tokens, "HEAD")
        patches.append(driver.stdoutScrollback())

        # Diff untracked files.
        # This requires fresh workdir status in RepoModel, which we probably have already
        # because this task requires the user to click on the workdir first.
        if not self.repoModel.workdirStatusReady or self.repoModel.workdirStale:
            raise NotImplementedError("Export workdir requires fresh status")

        for delta in self.repoModel.workdirUnstagedDeltas:
            if delta.status == GitStatus.Untracked:  # Scan for untracked files
                tokens = GitDriver.buildDiffCommand(delta)
                driver = yield from self.flowCallGit(*tokens, autoFail=False)
                patches.append(driver.stdoutScrollback())

        # Compose the patch
        assert all(not patch or patch.endswith("\n") for patch in patches)
        patch = "".join(patches)

        # Compose filename
        message = _p("patch file name, please keep it short",
                     "uncommitted changes on {commit}", commit=shortHash(self.repo.head_commit_id))
        fileName = f"{self.repo.repo_name()} - {message}.patch"

        yield from savePatch(self, patch, fileName)


class ExportPatchCollection(RepoTask):
    def flow(self, deltas: list[GitDelta]):
        names = []
        patches = []

        for delta in deltas:
            # Get filename stem
            file = delta.old if delta.status == GitStatus.Deleted else delta.new
            name = Path(file.path).stem
            names.append(name)

            # Get patch (run 'git diff')
            tokens = GitDriver.buildDiffCommand(delta)
            driver = yield from self.flowCallGit(*tokens, autoFail=False)
            patch = driver.stdoutScrollback()
            patches.append(patch)

        # Compose patch and filename
        assert all(not patch or patch.endswith("\n") for patch in patches)
        composed = "".join(patches)
        fileName = ", ".join(names) + ".patch"

        yield from savePatch(self, composed, fileName)
