import json
import os
import shutil
from abc import ABC, abstractmethod
from os.path import join as pjoin
from pathlib import Path
from contextlib import nullcontext
import requests
from app import utils as app_utils
from app.log import log_and_print
from app.data_structures import ReproResult
from app.task.task import PlainTask, SweTask, Task
from tempfile import NamedTemporaryFile
from app.utils import run_script_in_conda
import subprocess


class RawTask(ABC):
    @property
    @abstractmethod
    def task_id(self) -> str:
        raise NotImplementedError("abstract base class")

    @abstractmethod
    def to_task(self) -> Task:
        raise NotImplementedError("abstract base class")

    @abstractmethod
    def dump_meta_data(self, output_dir: str) -> None:
        raise NotImplementedError("abstract base class")


class RawSweTask(RawTask):
    """
    Encapsulate everything required to run one task.
    """

    def __init__(self, task_id: str, setup_info: dict, task_info: dict):
        # the instance id from SWE-bench
        self._task_id = task_id
        # setup_info (Dict): keys: ['repo_path', 'env_name', 'pre_install', 'install','test_cmd']
        self.setup_info = setup_info
        # task_info (Dict): keys: ['base_commit', 'hints_text', 'created_at',
        # 'test_patch', 'repo', 'problem_statement', 'version', 'instance_id',
        # 'FAIL_TO_PASS', 'PASS_TO_PASS', 'environment_setup_commit']
        self.task_info = task_info

    @property
    def task_id(self) -> str:
        return self._task_id

    def to_task(self) -> SweTask:
        task_id = self.task_id
        setup_info = self.setup_info
        task_info = self.task_info
        return SweTask(
            task_id=task_id,
            problem_statement=task_info["problem_statement"],
            repo_path=setup_info["repo_path"],
            env_name=setup_info["env_name"],
            pre_install_cmds=setup_info["pre_install"],
            install_cmd=setup_info["install"],
            # command to run the relevant tests,
            test_cmd=setup_info["test_cmd"],
            commit=task_info["base_commit"],
            repo_name=task_info["repo"],
            repo_version=task_info["version"],
            # modifications to the test suite for this task instance,
            test_patch=task_info["test_patch"],
            testcases_passing=task_info["PASS_TO_PASS"],
            testcases_failing=task_info["FAIL_TO_PASS"],
        )

    def dump_meta_data(self, output_dir: str):
        meta = {
            "task_id": self.task_id,
            "setup_info": self.setup_info,
            "task_info": self.task_info,
        }
        with open(pjoin(output_dir, "meta.json"), "w") as f:
            json.dump(meta, f, indent=4)
        with open(pjoin(output_dir, "problem_statement.txt"), "w") as f:
            f.write(self.task_info["problem_statement"])
        with open(pjoin(output_dir, "developer_patch.diff"), "w") as f:
            f.write(self.task_info["patch"])


class RawGithubTask(RawTask):
    """
    Encapsulate everything required to run ACR on a fresh issue from the internet.
    """

    def __init__(
        self,
        task_id: str,
        clone_link: str,
        commit_hash: str | None,
        issue_link: str,
        setup_dir: str,
    ):
        self._task_id = task_id
        self.clone_link = clone_link
        self.commit_hash = commit_hash
        self.issue_link = issue_link
        self.setup_dir = setup_dir
        self.clone_path = pjoin(self.setup_dir, self.task_id)
        self.problem_statement, self.created_at = self.fetch_issue()
        self.clone_repo()

    @property
    def task_id(self) -> str:
        return self._task_id

    def clone_repo(self):
        clone_path = Path(self.clone_path)
        if os.path.exists(clone_path):
            log_and_print(
                f"Path {clone_path} already exists. Removing it to get a fresh clone."
            )
            shutil.rmtree(clone_path)
        app_utils.clone_repo(self.clone_link, str(clone_path.parent), clone_path.name)
        log_and_print(f"Cloned source code to {clone_path}.")
        if self.commit_hash is None:
            # let's get the current commit hash
            with app_utils.cd(clone_path):
                self.commit_hash = app_utils.get_current_commit_hash()

    def dump_meta_data(self, output_dir: str):
        meta = {
            "task_info": {
                "base_commit": self.commit_hash,
                "created_at": self.created_at,
                "problem_statement": self.problem_statement,
                "instance_id": self.task_id,
            },
            "setup_info": {
                "repo_path": self.clone_path,
            },
        }

        meta_file = pjoin(output_dir, "meta.json")

        with open(meta_file, "w") as f:
            json.dump(meta, f, indent=4)

    def fetch_issue(self):
        if "github.com" not in self.issue_link:
            raise NotImplementedError("Only GitHub issues are supported for now.")

        retrieved_issue = self.fetch_github_issue(self.issue_link)

        if retrieved_issue is None:
            raise RuntimeError(
                f"Failed to retrieve issue information from {self.issue_link}"
            )

        title, body, created_at = retrieved_issue

        problem_statement = f"{title}\n{body}"

        return problem_statement, created_at

    @classmethod
    def fetch_github_issue(cls, issue_url: str) -> tuple[str, str, str]:
        """Extract owner, repo, and issue number from the URL"""

        # Example issue URL: https://github.com/owner/repo/issues/123

        _, owner, repo, _, issue_number = issue_url.rsplit("/", 4)

        api_url = f"https://api.github.com/repos/{owner}/{repo}/issues/{issue_number}"
        response = requests.get(api_url)

        if response.status_code != 200:
            raise RuntimeError(
                f"Failed to fetch issue information: {response.status_code}"
            )

        issue_info = response.json()

        title = issue_info["title"]
        body = issue_info["body"]
        created_at = issue_info["created_at"]

        return title, body, created_at

    def to_task(self) -> PlainTask:
        return PlainTask(
            commit_hash=self.commit_hash,
            local_path=self.clone_path,
            problem_statement=self.problem_statement,
        )


class RawLocalTask(RawTask):
    """
    Encapsulate everything required to run the agent on a local issue on the disk.
    """

    def __init__(self, task_id: str, local_repo: str, issue_file: str, task_data_path: str):
        with open(task_data_path, 'r') as f:
            whole_data = json.load(f)
        self.task_data_path = task_data_path
        self.target_file_rel_path = whole_data['target_module_path']
        self.prompt_list = whole_data['prompt']
        self.relavent_test_path = whole_data['relavent_test_path']
        self.GT_list = whole_data['full_function']
        self.function_name = whole_data['function_name']
        self.class_name = whole_data['class_name']
        self.project_name = whole_data['project_name']
        self.container_name = whole_data['container_name']
        self.start_line = whole_data['start_line']
        self.end_line = whole_data['end_line']
        self.test_data = whole_data['call_distance_test']

        self._task_id = task_id
        self.local_repo = local_repo
        self.issue_file = issue_file
        self.commit_hash = self.init_local_repo()
        self.problem_statement = self.read_issue_from_file()
        
        self.selected_test_list = []

    @property
    def task_id(self) -> str:
        return self._task_id
    
    def init_local_repo(self):
        with app_utils.cd(self.local_repo):
            if not app_utils.is_git_repo():
                # non git repo - let's make it a git repo first
                app_utils.initialize_git_repo_and_commit()
            commit = app_utils.get_current_commit_hash()
        return commit

    def read_issue_from_file(self) -> str:
        # ignore encoding errors so at least we can have some issue content
        issue = Path(self.issue_file).read_text(errors="ignore")
        return issue

    def dump_meta_data(self, output_dir: str):
        meta = {
            "task_info": {
                "base_commit": self.commit_hash,
                "problem_statement": self.problem_statement,
                "instance_id": self.task_id,
            },
            "setup_info": {
                "repo_path": self.local_repo,
            },
        }

        meta_file = pjoin(output_dir, "meta.json")

        with open(meta_file, "w") as f:
            json.dump(meta, f, indent=4)

    def to_task(self):
        # return PlainTask(
        #     env_name = self.env_name,
        #     commit_hash=self.commit_hash,
        #     local_path=self.local_repo,
        #     problem_statement=self.problem_statement,
        # )
        return self


    @property
    def project_path(self) -> str:
        return self.local_repo

    def setup_project(self) -> None: # this will restore the ground truth of target code
        with app_utils.cd(self.project_path):
            app_utils.repo_reset_and_clean_checkout(self.commit_hash)
            pass

    def reset_project(self) -> None:
        with app_utils.cd(self.project_path):
            app_utils.repo_reset_and_clean_checkout(self.commit_hash)
            pass

    def get_issue_statement(self) -> str:
        return self.problem_statement

    def validate(self, patch_content: str) -> tuple[bool, str, str, str]:
        raise NotImplementedError("Cannot do validation for live issues for now")
    
    # the following function is added by 
    def execute_reproducer(
        self, test_content: str, patch_content: str | None = None
    ) -> ReproResult:
        cm = nullcontext() if patch_content is None else self.apply_patch(patch_content)
        with cm:
            with NamedTemporaryFile(
                buffering=0, prefix="reproducer-", suffix=".py"
            ) as f:
                f.write(test_content.encode())
                try:
                    cp = run_script_in_conda(
                        [f.name],
                        self.env_name,
                        cwd=self.project_path,
                        text=True,
                        capture_output=True,
                        timeout=120,  # 2 min for reproducer should be enough
                    )
                    cp_stdout = cp.stdout
                    cp_stderr = cp.stderr
                    cp_returncode = cp.returncode
                except subprocess.TimeoutExpired:
                    cp_stdout = ""
                    cp_stderr = "Test execution timeout."
                    cp_returncode = -1

        # stderr can be very long; truncate it so we dont exceed model limit
        stderr_result = str(cp_stderr)
        stderr_lines = stderr_result.splitlines()
        if len(stderr_lines) > 100:
            # take first 50 and last 50 lines
            stderr_result = "\n".join(stderr_lines[:50] + ["..."] + stderr_lines[-50:])
        return ReproResult(cp_stdout, stderr_result, cp_returncode)