import json
import re
import shutil
import subprocess
import tarfile
from pathlib import Path
from typing import Any

import httpx
import yaml
from fastapi import HTTPException

from mosaygent.command_runner import run_command
from mosaygent.logger import get_logger

logger = get_logger(__name__)

TEMPLATE_OWNER = "mosayic-io"
TEMPLATE_REPO = "mobile-app"
API_TEMPLATE_REPO = "python-api"

class GitHubService:
    """Service for GitHub CLI operations and repository management."""

    async def check_auth(self) -> dict:
        """Check GitHub CLI installation and auth status."""
        logger.info("Checking GitHub CLI installation and auth status")

        gh_result = await run_command(["gh", "--version"], timeout=5, raise_on_error=False)

        if not gh_result.success:
            logger.warning("GitHub CLI is not installed")
            return {"installed": False}

        version_match = re.search(r'gh version (\S+)', gh_result.stdout)
        version = version_match.group(1) if version_match else gh_result.stdout.strip()
        logger.info(f"GitHub CLI is installed: {version}")

        auth_result = await run_command(["gh", "auth", "status"], timeout=5, raise_on_error=False)

        if auth_result.success:
            logger.info("User is logged in to GitHub CLI")
            return {
                "installed": True,
                "version": version,
                "logged_in": True,
                "output": auth_result.stdout.strip(),
            }

        logger.info("User is not logged in to GitHub CLI")
        return {"installed": True, "version": version, "logged_in": False}

    async def get_accounts(self) -> dict:
        """List user's personal account and organizations they can create repos in."""
        logger.info("Fetching user accounts and organizations")

        user_result = await run_command(
            ["gh", "api", "user", "--jq", ".login"],
            timeout=10,
            raise_on_error=False
        )

        if not user_result.success:
            logger.error("Failed to get authenticated user")
            raise PermissionError("Not authenticated with GitHub CLI. Run 'gh auth login' first.")

        username = user_result.stdout.strip()
        logger.info(f"Authenticated user: {username}")

        orgs_result = await run_command(
            ["gh", "api", "user/memberships/orgs", "--jq", "[.[] | select(.role == \"admin\" or .permissions.can_create_repository == true) | .organization.login]"],
            timeout=10,
            raise_on_error=False
        )

        orgs = []
        if orgs_result.success and orgs_result.stdout.strip():
            try:
                orgs = json.loads(orgs_result.stdout.strip())
            except json.JSONDecodeError:
                logger.warning(f"Failed to parse orgs response: {orgs_result.stdout}")

        if not orgs:
            orgs_simple_result = await run_command(
                ["gh", "org", "list"],
                timeout=10,
                raise_on_error=False
            )
            if orgs_simple_result.success and orgs_simple_result.stdout.strip():
                orgs = [org.strip() for org in orgs_simple_result.stdout.strip().split("\n") if org.strip()]

        logger.info(f"Found {len(orgs)} organizations: {orgs}")

        accounts = [{"name": username, "type": "user"}]
        for org in orgs:
            accounts.append({"name": org, "type": "organization"})

        return {
            "username": username,
            "accounts": accounts,
        }

    async def check_repo_exists(self, repo_name: str) -> bool:
        """Check if a repository already exists on GitHub."""
        logger.info(f"Checking if repo '{repo_name}' exists on GitHub")

        result = await run_command(
            ["gh", "repo", "view", repo_name],
            timeout=10,
            raise_on_error=False
        )

        exists = result.success
        logger.info(f"Repository '{repo_name}' exists: {exists}")
        return exists

    async def create_repo(
        self,
        full_repo_name: str,
        source_dir: Path,
        private: bool = True,
    ) -> str:
        """Create a new GitHub repository and push source."""
        logger.info(f"Creating new GitHub repository: {full_repo_name}")
        visibility = "--private" if private else "--public"

        result = await run_command(
            ["gh", "repo", "create", full_repo_name, visibility, "--source", str(source_dir), "--push"],
            cwd=str(source_dir),
            timeout=60
        )
        logger.info(f"Repository created and pushed: {result.stdout}")
        return result.stdout

    async def push_to_existing_repo(self, full_repo_name: str, source_dir: Path) -> None:
        """Add remote and push to an existing GitHub repository."""
        logger.info(f"Repository '{full_repo_name}' already exists, adding remote and pushing")

        await run_command(
            ["git", "remote", "add", "origin", f"git@github.com:{full_repo_name}.git"],
            cwd=str(source_dir),
            timeout=10,
            raise_on_error=False
        )
        await run_command(
            ["git", "push", "-u", "origin", "main"],
            cwd=str(source_dir),
            timeout=60
        )

    async def create_or_push_to_github(
        self,
        full_repo_name: str,
        source_dir: Path,
        private: bool = True,
    ) -> bool:
        """Create a new GitHub repo or push to existing one. Returns True if repo was created."""
        repo_exists = await self.check_repo_exists(full_repo_name)

        if not repo_exists:
            await self.create_repo(full_repo_name, source_dir, private)
        else:
            await self.push_to_existing_repo(full_repo_name, source_dir)

        return not repo_exists

    async def init_git_and_push(
        self,
        repo_path: Path,
        owner: str,
        private: bool = True,
    ) -> dict:
        """Initialize git, commit, and push to a new or existing GitHub repository."""
        repo_name = repo_path.name
        full_repo_name = f"{owner}/{repo_name}"
        logger.info(f"Initializing git and pushing to '{full_repo_name}' from {repo_path}")

        if not repo_path.exists():
            logger.error(f"Repository path does not exist: {repo_path}")
            raise FileNotFoundError(f"Repository path does not exist: {repo_path}")

        if not repo_path.is_dir():
            logger.error(f"Repository path is not a directory: {repo_path}")
            raise NotADirectoryError(f"Repository path is not a directory: {repo_path}")

        git_dir = repo_path / ".git"
        if git_dir.exists():
            logger.error(f"Git repository already exists at {repo_path}")
            raise FileExistsError(f"Git repository already initialized at {repo_path}")

        logger.info("Initializing git repository")
        await run_command(["git", "init"], cwd=str(repo_path), timeout=10)

        logger.info("Adding all files to git")
        await run_command(["git", "add", "-A"], cwd=str(repo_path), timeout=30)

        logger.info("Creating initial commit")
        await run_command(
            ["git", "commit", "-m", "Initial commit from Mosayic"],
            cwd=str(repo_path),
            timeout=30
        )

        created_on_github = await self.create_or_push_to_github(full_repo_name, repo_path, private)

        return {
            "repo_name": full_repo_name,
            "owner": owner,
            "path": str(repo_path),
            "created_on_github": created_on_github,
        }

    async def get_latest_release_tag(self, owner: str, repo: str) -> str:
        """Fetch the latest release tag from GitHub API."""
        url = f"https://api.github.com/repos/{owner}/{repo}/releases/latest"
        logger.info(f"Fetching latest release tag from {url}")

        async with httpx.AsyncClient() as client:
            response = await client.get(url, timeout=30)
            if response.status_code != 200:
                logger.error(f"Failed to fetch release info: {response.status_code}")
                raise RuntimeError(f"Failed to fetch release info from GitHub: {response.status_code}")

            data = response.json()
            tag = data.get("tag_name")
            if not tag:
                raise RuntimeError("No tag_name found in release")

            logger.info(f"Latest release tag: {tag}")
            return tag

    async def download_and_extract_release(
        self,
        owner: str,
        repo: str,
        tag: str,
        target_dir: Path,
    ) -> None:
        """Download and extract the release tarball."""
        url = f"https://github.com/{owner}/{repo}/archive/refs/tags/{tag}.tar.gz"
        logger.info(f"Downloading release from {url}")

        async with httpx.AsyncClient(follow_redirects=True) as client:
            response = await client.get(url, timeout=120)
            if response.status_code != 200:
                logger.error(f"Failed to download release: {response.status_code}")
                raise RuntimeError(f"Failed to download release tarball: {response.status_code}")

            tarball_path = target_dir.parent / "source.tar.gz"
            tarball_path.write_bytes(response.content)
            logger.info(f"Downloaded tarball to {tarball_path}")

        logger.info(f"Extracting tarball to {target_dir.parent}")
        with tarfile.open(tarball_path, "r:gz") as tar:
            tar.extractall(path=target_dir.parent)

        tag_suffix = tag.lstrip("v")
        extracted_dir = target_dir.parent / f"{repo}-{tag_suffix}"

        if not extracted_dir.exists():
            extracted_dir = target_dir.parent / f"{repo}-{tag}"

        if not extracted_dir.exists():
            logger.error(f"Extracted directory not found: {extracted_dir}")
            raise RuntimeError("Failed to find extracted directory")

        logger.info(f"Moving {extracted_dir} to {target_dir}")
        shutil.move(str(extracted_dir), str(target_dir))

        tarball_path.unlink()
        logger.info("Cleaned up tarball")

    async def init_repo_from_template(
        self,
        parent_dir: Path,
        repo_name: str,
        owner: str,
        private: bool,
        app_name: str,
        slug: str,
        bundle_id: str,
    ) -> dict:
        """Initialize a new git repository from the mobile-app template."""
        target_dir = parent_dir / repo_name
        full_repo_name = f"{owner}/{repo_name}"
        logger.info(f"Initializing new repo '{full_repo_name}' at {target_dir}")

        if target_dir.exists():
            logger.error(f"Target directory already exists: {target_dir}")
            raise FileExistsError(f"Target directory already exists: {target_dir}")

        target_dir.parent.mkdir(parents=True, exist_ok=True)

        try:
            tag = await self.get_latest_release_tag(TEMPLATE_OWNER, TEMPLATE_REPO)
            await self.download_and_extract_release(TEMPLATE_OWNER, TEMPLATE_REPO, tag, target_dir)

            env_example = target_dir / ".env.example"
            env_file = target_dir / ".env"
            if env_example.exists():
                logger.info("Renaming .env.example to .env")
                env_example.rename(env_file)

            logger.info("Running npm install")
            await run_command(["npm", "install"], cwd=str(target_dir), timeout=300)

            app_json_path = target_dir / "app.json"
            self._update_app_json(app_json_path, app_name, slug, bundle_id)

            logger.info("Initializing git repository")
            await run_command(["git", "init"], cwd=str(target_dir), timeout=10)

            logger.info("Initializing EAS project")
            await run_command(
                ["eas", "init", "--force", "--non-interactive"],
                cwd=str(target_dir),
                timeout=60
            )
            logger.info("EAS project linked successfully")

            logger.info("Adding all files to git")
            await run_command(["git", "add", "-A"], cwd=str(target_dir), timeout=30)

            logger.info("Creating initial commit")
            await run_command(
                ["git", "commit", "-m", "Initial commit from Mosayic"],
                cwd=str(target_dir),
                timeout=30
            )

            created_on_github = await self.create_or_push_to_github(full_repo_name, target_dir, private)

            return {
                "repo_name": full_repo_name,
                "owner": owner,
                "path": str(target_dir),
                "template_version": tag,
                "created_on_github": created_on_github,
                "app_name": app_name,
                "eas_project_id": slug,
                "bundle_id": bundle_id,
            }

        except Exception as e:
            logger.exception(f"Failed to initialize repository: {e}")
            if target_dir.exists():
                shutil.rmtree(target_dir)
                logger.info(f"Cleaned up failed directory: {target_dir}")
            raise

    async def init_api_from_template(self, parent_dir: Path) -> dict:
        """Initialize a new API project from the python-api template."""
        dir_name = "api"
        target_dir = parent_dir / dir_name
        logger.info(f"Initializing API project at {target_dir}")

        if target_dir.exists():
            logger.error(f"Target directory already exists: {target_dir}")
            raise FileExistsError(f"Directory already exists: {target_dir}")

        try:
            tag = await self.get_latest_release_tag(TEMPLATE_OWNER, API_TEMPLATE_REPO)
            await self.download_and_extract_release(TEMPLATE_OWNER, API_TEMPLATE_REPO, tag, target_dir)

            return {
                "path": str(target_dir),
                "template_version": tag,
            }
        except Exception as e:
            logger.exception(f"Failed to initialize API project: {e}")
            if target_dir.exists():
                shutil.rmtree(target_dir)
                logger.info(f"Cleaned up failed directory: {target_dir}")
            raise

    async def set_actions_secret(self, secret_name: str, secret_value: str, repo: str) -> dict:
        """
        Set a secret in GitHub Actions for the specified repository.

        Args:
            secret_name: Name of the secret (e.g., 'API_KEY')
            secret_value: Value of the secret
            repo: Repository in 'owner/repo' format

        Returns:
            Dictionary with operation result
        """
        logger.info(f"Setting GitHub Actions secret '{secret_name}' for repo '{repo}'")

        cmd = ["gh", "secret", "set", secret_name, "--repo", repo]

        try:
            process = subprocess.run(
                cmd,
                input=secret_value,
                capture_output=True,
                text=True,
                timeout=30
            )

            if process.returncode != 0:
                logger.error(f"Failed to set GitHub Actions secret '{secret_name}': {process.stderr}")
                return {
                    "secret_name": secret_name,
                    "success": False,
                    "error": process.stderr.strip()
                }

            logger.info(f"Successfully set GitHub Actions secret '{secret_name}'")
            return {
                "secret_name": secret_name,
                "success": True
            }

        except FileNotFoundError:
            logger.error("GitHub CLI (gh) not found")
            return {
                "secret_name": secret_name,
                "success": False,
                "error": "GitHub CLI (gh) not found. Is it installed and in PATH?"
            }
        except subprocess.TimeoutExpired:
            logger.error(f"Setting GitHub Actions secret '{secret_name}' timed out")
            return {
                "secret_name": secret_name,
                "success": False,
                "error": "Timeout after 30 seconds"
            }

    def _update_app_json(
        self,
        app_json_path: Path,
        app_name: str,
        slug: str,
        bundle_id: str,
    ) -> None:
        """Update app.json with the new app configuration values."""
        logger.info(f"Updating app.json at {app_json_path}")
        logger.info(f"Input values - app_name: {app_name}, slug: {slug}, bundle_id: {bundle_id}")

        if not app_json_path.exists():
            logger.error(f"app.json not found at {app_json_path}")
            raise FileNotFoundError(f"app.json not found at {app_json_path}")

        content = app_json_path.read_text()
        data = json.loads(content)

        expo = data.get("expo", {})
        logger.info(f"Current expo.slug before update: {expo.get('slug')}")

        expo["name"] = app_name
        expo["slug"] = slug
        expo["scheme"] = app_name.lower().replace(" ", "").replace("-", "")

        logger.info(f"New expo.slug after update: {expo['slug']}")

        if "ios" in expo:
            expo["ios"]["bundleIdentifier"] = bundle_id
            logger.debug(f"Set iOS bundleIdentifier to {bundle_id}")

        if "android" in expo:
            expo["android"]["package"] = bundle_id
            logger.debug(f"Set Android package to {bundle_id}")

        data["expo"] = expo

        app_json_path.write_text(json.dumps(data, indent=2) + "\n")
        logger.info(f"Successfully updated app.json: name={app_name}, slug={slug}, bundle_id={bundle_id}")

    def _update_eas_json_supabase_keys(
        self,
        mobile_app_directory: str,
        supabase_anon_key: str | None,
        supabase_url: str | None
    ) -> bool:
        """Update eas.json production track with Supabase keys."""
        mobile_app_dir = Path(mobile_app_directory).resolve()
        eas_json_path = mobile_app_dir / "eas.json"

        if not eas_json_path.exists():
            logger.warning(f"eas.json not found at {eas_json_path}, skipping update")
            return False

        if not supabase_anon_key and not supabase_url:
            logger.info("No Supabase keys provided, skipping eas.json update")
            return False

        logger.info(f"Updating eas.json at {eas_json_path}")

        try:
            content = eas_json_path.read_text()
            data = json.loads(content)

            production_env = data.get("build", {}).get("production", {}).get("env", {})

            if supabase_anon_key:
                production_env["EXPO_PUBLIC_SUPABASE_PUB_KEY"] = supabase_anon_key
                logger.info("Set EXPO_PUBLIC_SUPABASE_PUB_KEY in eas.json production track")

            if supabase_url:
                production_env["EXPO_PUBLIC_SUPABASE_URL"] = supabase_url
                logger.info("Set EXPO_PUBLIC_SUPABASE_URL in eas.json production track")

            if "build" not in data:
                data["build"] = {}
            if "production" not in data["build"]:
                data["build"]["production"] = {}
            data["build"]["production"]["env"] = production_env

            eas_json_path.write_text(json.dumps(data, indent=2) + "\n")
            logger.info("Successfully updated eas.json with Supabase keys")
            return True

        except json.JSONDecodeError as e:
            logger.error(f"Failed to parse eas.json: {e}")
            return False
        except Exception as e:
            logger.exception(f"Failed to update eas.json: {e}")
            return False

    async def set_env_variables(
        self,
        env_variables: dict[str, str],
        working_directory: str,
        mobile_app_directory: str | None = None,
    ) -> dict:
        """Create or update .env.prod.yaml and optionally update eas.json with Supabase keys."""
        logger.info(f"Setting {len(env_variables)} env variables in .env.prod.yaml")

        work_dir = Path(working_directory).resolve()
        logger.debug(f"Creating environment file in directory: {work_dir}")

        if not work_dir.exists():
            logger.error(f"Working directory does not exist: {work_dir}")
            raise HTTPException(
                status_code=400,
                detail=f"Working directory does not exist: {work_dir}"
            )

        env_prod_yaml_path = work_dir / ".env.prod.yaml"
        logger.debug(f"Creating .env.prod.yaml file at: {env_prod_yaml_path}")

        try:
            with open(env_prod_yaml_path, 'w') as f:
                yaml.dump(env_variables, f, default_flow_style=False, sort_keys=False)
            logger.info(f"Successfully created/updated .env.prod.yaml with {len(env_variables)} env variables")
        except Exception as e:
            logger.exception(f"Failed to write .env.prod.yaml file: {e}")
            raise HTTPException(
                status_code=500,
                detail=f"Failed to write .env.prod.yaml file: {str(e)}"
            )

        eas_json_updated = False
        if mobile_app_directory:
            eas_json_updated = self._update_eas_json_supabase_keys(
                mobile_app_directory=mobile_app_directory,
                supabase_anon_key=env_variables.get("SUPABASE_ANON_KEY"),
                supabase_url=env_variables.get("SUPABASE_URL")
            )

        logger.info(f"Operation completed. Env variables: {len(env_variables)}, EAS JSON: {eas_json_updated}")

        return {
            "env_variables_count": len(env_variables),
            "eas_json_updated": eas_json_updated,
        }

    async def set_secret(
        self,
        secret_name: str,
        secret_value: str,
        gh_repo: str,
        firebase_project_id: str | None = None,
    ) -> dict:
        """Set a secret in both Firebase/GCP and GitHub Actions."""
        logger.info(f"Setting secret '{secret_name}' for Firebase and GitHub repo '{gh_repo}'")

        if not firebase_project_id:
            project_result = await run_command(
                ["gcloud", "config", "get-value", "project"],
                timeout=5,
                raise_on_error=False
            )

            if not project_result.success or not project_result.stdout.strip():
                raise HTTPException(
                    status_code=400,
                    detail="No GCloud project configured. Set a project first or provide firebase_project_id"
                )

            firebase_project_id = project_result.stdout.strip()

        firebase_success = False
        firebase_error = None

        try:
            process = subprocess.run(
                ["firebase", "functions:secrets:set", secret_name, "--project", firebase_project_id, "--data-file", "-"],
                input=secret_value,
                capture_output=True,
                text=True,
                timeout=60
            )

            if process.returncode != 0:
                firebase_error = process.stderr.strip()
            else:
                firebase_success = True

        except FileNotFoundError:
            firebase_error = "Firebase CLI not found. Is it installed and in PATH?"
        except subprocess.TimeoutExpired:
            firebase_error = "Timeout after 60 seconds"
        except Exception as e:
            logger.exception(f"Unexpected error uploading secret to Firebase: {e}")
            firebase_error = str(e)

        gh_result = await self.set_actions_secret(
            secret_name=secret_name,
            secret_value=secret_value,
            repo=gh_repo
        )
        gh_success = gh_result.get("success", False)
        gh_error = gh_result.get("error")

        logger.info(f"Secret '{secret_name}' set - Firebase: {firebase_success}, GitHub: {gh_success}")

        response: dict[str, Any] = {
            "secret_name": secret_name,
            "firebase_project_id": firebase_project_id,
            "firebase_success": firebase_success,
            "gh_repo": gh_repo,
            "gh_actions_success": gh_success,
        }

        if firebase_error:
            response["firebase_error"] = firebase_error

        if gh_error:
            response["gh_actions_error"] = gh_error

        return response

    async def get_env_variables(self, working_directory: str) -> dict:
        """Read environment variables from .env.prod.yaml file."""
        logger.info(f"Reading env variables from .env.prod.yaml")

        work_dir = Path(working_directory).resolve()
        logger.debug(f"Reading environment file from directory: {work_dir}")

        if not work_dir.exists():
            logger.error(f"Working directory does not exist: {work_dir}")
            raise HTTPException(
                status_code=400,
                detail=f"Working directory does not exist: {work_dir}"
            )

        env_file_path = work_dir / ".env.prod.yaml"
        logger.debug(f"Reading .env.prod.yaml file at: {env_file_path}")

        if not env_file_path.exists():
            logger.info(f".env.prod.yaml file not found at: {env_file_path}, returning empty response")
            return {
                "env_variables": {},
                "env_variables_count": 0
            }

        try:
            with open(env_file_path, 'r') as f:
                env_variables = yaml.safe_load(f)

            if not isinstance(env_variables, dict):
                logger.error(f"Invalid YAML format in .env.prod.yaml: expected dict, got {type(env_variables)}")
                raise HTTPException(
                    status_code=500,
                    detail="Invalid YAML format: expected dictionary of key-value pairs"
                )

            logger.info(f"Successfully read {len(env_variables)} env variables from .env.prod.yaml")

            return {
                "env_variables": env_variables,
                "env_variables_count": len(env_variables)
            }

        except yaml.YAMLError as e:
            logger.exception(f"Failed to parse .env.prod.yaml file: {e}")
            raise HTTPException(
                status_code=500,
                detail=f"Failed to parse YAML file: {str(e)}"
            )
        except HTTPException:
            raise
        except Exception as e:
            logger.exception(f"Failed to read .env.prod.yaml file: {e}")
            raise HTTPException(
                status_code=500,
                detail=f"Failed to read .env.prod.yaml file: {str(e)}"
            )
