from __future__ import annotations

from datetime import datetime
import os
import shutil
import shlex
import subprocess
from typing import Any

from identity import load_agent_token
from logger import get_logger
from config import Config
from api_client import ApiClient

log = get_logger("actions.pipeline")


def resolve_binary(binary: str) -> str:
    path = shutil.which(binary)
    return path if path else binary


def _step_git_pull(step: dict, context: dict, branch: str) -> dict[str, str]:
    git_bin = resolve_binary("git")
    target_branch = step.get("config", {}).get("branch") or branch or "main"
    cwd = context.get("repo_path")

    # 1. Run git checkout {branch}
    try:
        res_checkout = subprocess.run(
            [git_bin, "checkout", target_branch],
            cwd=cwd,
            capture_output=True,
            text=True,
            shell=False,
            timeout=60
        )
        checkout_out = f"git checkout {target_branch}:\ncode: {res_checkout.returncode}\nstdout:\n{res_checkout.stdout}\nstderr:\n{res_checkout.stderr}\n"
        if res_checkout.returncode != 0:
            return {"status": "failed", "output": checkout_out}
    except subprocess.TimeoutExpired as exc:
        return {
            "status": "failed",
            "output": f"git checkout timed out after 60 seconds\nstdout:\n{exc.stdout or ''}\nstderr:\n{exc.stderr or ''}"
        }

    # 2. Run git pull origin {branch}
    try:
        res_pull = subprocess.run(
            [git_bin, "pull", "origin", target_branch],
            cwd=cwd,
            capture_output=True,
            text=True,
            shell=False,
            timeout=60
        )
        pull_out = f"git pull origin {target_branch}:\ncode: {res_pull.returncode}\nstdout:\n{res_pull.stdout}\nstderr:\n{res_pull.stderr}\n"
        combined_output = checkout_out + "\n" + pull_out
        if res_pull.returncode != 0:
            return {"status": "failed", "output": combined_output}
        return {"status": "succeeded", "output": combined_output}
    except subprocess.TimeoutExpired as exc:
        return {
            "status": "failed",
            "output": checkout_out + f"\ngit pull timed out after 60 seconds\nstdout:\n{exc.stdout or ''}\nstderr:\n{exc.stderr or ''}"
        }


def _step_run_command(step: dict, context: dict) -> dict[str, str]:
    cmd_str = step.get("config", {}).get("command") or step.get("command")
    if not cmd_str:
        return {"status": "failed", "output": "No command specified"}
    if len(cmd_str) > 1000:
        return {"status": "failed", "output": "Command exceeds maximum length of 1000 characters"}

    args = shlex.split(cmd_str)
    if not args:
        return {"status": "failed", "output": "Empty command after parsing"}

    args[0] = resolve_binary(args[0])
    cwd = context.get("repo_path")
    try:
        res = subprocess.run(
            args,
            cwd=cwd,
            capture_output=True,
            text=True,
            shell=False,
            timeout=300
        )
        output = f"stdout:\n{res.stdout}\nstderr:\n{res.stderr}"
        if res.returncode == 0:
            return {"status": "succeeded", "output": output}
        return {"status": "failed", "output": f"Command failed with exit code {res.returncode}\n{output}"}
    except subprocess.TimeoutExpired as exc:
        return {
            "status": "failed",
            "output": f"Command timed out after 300 seconds\nstdout:\n{exc.stdout or ''}\nstderr:\n{exc.stderr or ''}"
        }


def _step_npm(step: dict, context: dict) -> dict[str, str]:
    npm_bin = resolve_binary("npm")
    action = step.get("config", {}).get("action") or "install"
    args = [npm_bin] + shlex.split(action)
    cwd = context.get("repo_path")
    try:
        res = subprocess.run(
            args,
            cwd=cwd,
            capture_output=True,
            text=True,
            shell=False,
            timeout=300
        )
        output = f"stdout:\n{res.stdout}\nstderr:\n{res.stderr}"
        if res.returncode == 0:
            return {"status": "succeeded", "output": output}
        return {"status": "failed", "output": f"npm failed with exit code {res.returncode}\n{output}"}
    except subprocess.TimeoutExpired as exc:
        return {
            "status": "failed",
            "output": f"npm timed out after 300 seconds\nstdout:\n{exc.stdout or ''}\nstderr:\n{exc.stderr or ''}"
        }


def _step_composer(step: dict, context: dict) -> dict[str, str]:
    composer_bin = resolve_binary("composer")
    action = step.get("config", {}).get("action") or "install"
    args = [composer_bin] + shlex.split(action)
    cwd = context.get("repo_path")
    try:
        res = subprocess.run(
            args,
            cwd=cwd,
            capture_output=True,
            text=True,
            shell=False,
            timeout=300
        )
        output = f"stdout:\n{res.stdout}\nstderr:\n{res.stderr}"
        if res.returncode == 0:
            return {"status": "succeeded", "output": output}
        return {"status": "failed", "output": f"composer failed with exit code {res.returncode}\n{output}"}
    except subprocess.TimeoutExpired as exc:
        return {
            "status": "failed",
            "output": f"composer timed out after 300 seconds\nstdout:\n{exc.stdout or ''}\nstderr:\n{exc.stderr or ''}"
        }


def _step_artisan(step: dict, context: dict) -> dict[str, str]:
    php_bin = resolve_binary("php")
    cmd_str = step.get("config", {}).get("command") or "migrate --force"
    args = [php_bin, "artisan"] + shlex.split(cmd_str)
    cwd = context.get("repo_path")
    try:
        res = subprocess.run(
            args,
            cwd=cwd,
            capture_output=True,
            text=True,
            shell=False,
            timeout=120
        )
        output = f"stdout:\n{res.stdout}\nstderr:\n{res.stderr}"
        if res.returncode == 0:
            return {"status": "succeeded", "output": output}
        return {"status": "failed", "output": f"php artisan failed with exit code {res.returncode}\n{output}"}
    except subprocess.TimeoutExpired as exc:
        return {
            "status": "failed",
            "output": f"php artisan timed out after 120 seconds\nstdout:\n{exc.stdout or ''}\nstderr:\n{exc.stderr or ''}"
        }


def _step_pm2(step: dict, context: dict) -> dict[str, str]:
    pm2_bin = resolve_binary("pm2")
    action = step.get("config", {}).get("action") or "restart"
    app = step.get("config", {}).get("app") or "all"
    args = [pm2_bin] + shlex.split(action) + shlex.split(app)
    cwd = context.get("repo_path")
    try:
        res = subprocess.run(
            args,
            cwd=cwd,
            capture_output=True,
            text=True,
            shell=False,
            timeout=60
        )
        output = f"stdout:\n{res.stdout}\nstderr:\n{res.stderr}"
        if res.returncode == 0:
            return {"status": "succeeded", "output": output}
        return {"status": "failed", "output": f"pm2 failed with exit code {res.returncode}\n{output}"}
    except subprocess.TimeoutExpired as exc:
        return {
            "status": "failed",
            "output": f"pm2 timed out after 60 seconds\nstdout:\n{exc.stdout or ''}\nstderr:\n{exc.stderr or ''}"
        }


def _step_health_check(step: dict, context: dict) -> dict[str, str]:
    curl_bin = resolve_binary("curl")
    url = step.get("config", {}).get("url") or "http://localhost"
    args = [curl_bin, "-sf", url]
    try:
        res = subprocess.run(
            args,
            capture_output=True,
            text=True,
            shell=False,
            timeout=30
        )
        output = f"stdout:\n{res.stdout}\nstderr:\n{res.stderr}"
        if res.returncode == 0:
            return {"status": "succeeded", "output": output}
        return {"status": "failed", "output": f"Health check failed with exit code {res.returncode}\n{output}"}
    except subprocess.TimeoutExpired as exc:
        return {
            "status": "failed",
            "output": f"Health check timed out after 30 seconds\nstdout:\n{exc.stdout or ''}\nstderr:\n{exc.stderr or ''}"
        }


def _step_notification(step: dict, context: dict) -> dict[str, str]:
    msg = step.get("config", {}).get("message") or "Notification step executed"
    log.info("Pipeline Notification: %s", msg)
    return {"status": "succeeded", "output": f"Notification: {msg}"}


STEP_HANDLERS = {
    "git-pull": _step_git_pull,
    "run-command": _step_run_command,
    "npm": _step_npm,
    "composer": _step_composer,
    "artisan": _step_artisan,
    "pm2": _step_pm2,
    "health-check": _step_health_check,
    "notification": _step_notification,
}


def handle_pipeline_run(payload: dict, config: Config, api_client: ApiClient) -> dict:
    pipeline_id = payload.get("pipeline_id")
    run_id = payload.get("run_id")
    steps = payload.get("steps") or []
    branch = payload.get("branch") or "main"

    token = load_agent_token(config)

    # Initialize context
    context = {}
    if "repo_path" in payload:
        context["repo_path"] = payload["repo_path"]
    context["branch"] = branch

    # 1. Initialize step logs to "pending"
    logs = []
    for idx, step in enumerate(steps):
        logs.append({
            "step_index": idx,
            "step_name": step.get("label") or step.get("type") or f"Step {idx}",
            "status": "pending",
            "output": None,
            "started_at": None,
            "finished_at": None
        })

    # 2. Inform API that run is "running" before starting
    try:
        api_client.update_pipeline_run(run_id, "running", logs, token)
    except Exception as e:
        log.warning("failed to initialize pipeline run status on backend: %s", e)

    current_run_success = True

    # 3. Iterate and execute each step
    for idx, step in enumerate(steps):
        step_type = step.get("type")

        # Parse condition
        condition = step.get("condition") or {}
        if isinstance(condition, dict):
            cond_type = condition.get("type", "on-success")
        else:
            cond_type = str(condition)

        # Check conditional execution
        should_run = True
        if cond_type == "on-success" and not current_run_success:
            should_run = False
        elif cond_type == "on-failure" and current_run_success:
            should_run = False

        if should_run:
            # Set state to running
            logs[idx]["status"] = "running"
            logs[idx]["started_at"] = datetime.utcnow().isoformat() + "Z"
            try:
                api_client.update_pipeline_run(run_id, "running", logs, token)
            except Exception as e:
                log.warning("failed to update pipeline run step running state: %s", e)

            # Invoke step handler
            if step_type not in STEP_HANDLERS:
                res = {"status": "failed", "output": f"Unsupported step type: {step_type}"}
            else:
                handler = STEP_HANDLERS[step_type]
                try:
                    if step_type == "git-pull":
                        res = handler(step, context, branch)
                    else:
                        res = handler(step, context)
                except Exception as e:
                    res = {"status": "failed", "output": f"Step execution handler exception: {str(e)}"}

            # Update logs with result
            logs[idx]["status"] = res["status"]
            logs[idx]["output"] = res["output"]
            logs[idx]["finished_at"] = datetime.utcnow().isoformat() + "Z"

            if res["status"] == "failed":
                current_run_success = False
        else:
            # Mark step as skipped
            logs[idx]["status"] = "skipped"
            logs[idx]["output"] = f"Skipped due to condition {cond_type}"

        # Report status back to API
        try:
            api_client.update_pipeline_run(run_id, "running", logs, token)
        except Exception as e:
            log.warning("failed to update pipeline run step finished state: %s", e)

    # 4. Report final status
    final_status = "succeeded" if current_run_success else "failed"
    try:
        api_client.update_pipeline_run(run_id, final_status, logs, token)
    except Exception as e:
        log.warning("failed to update pipeline run final status: %s", e)

    return {"success": current_run_success, "status": final_status}
