"""
koci Core Models

Dataclasses representing the pipeline structure:
Pipeline -> Stage -> Job -> Step

Also includes Service and Artifact models.
"""

from dataclasses import dataclass, field
from typing import Optional, Any
from enum import Enum


class StepType(Enum):
    """Type of step to execute."""
    RUN = "run"
    CHECKOUT = "checkout"
    CACHE = "cache"
    ARTIFACT = "artifact"


class JobStatus(Enum):
    """Status of a job during execution."""
    PENDING = "pending"
    RUNNING = "running"
    SUCCESS = "success"
    FAILED = "failed"
    SKIPPED = "skipped"
    CANCELLED = "cancelled"


@dataclass
class Step:
    """
    A single step within a job.

    Examples:
        - run: npm install
        - run: |
            echo "Multi-line"
            echo "command"
        - name: Install dependencies
          run: npm install
    """
    run: str
    name: Optional[str] = None
    working_directory: Optional[str] = None
    env: dict[str, str] = field(default_factory=dict)
    shell: Optional[str] = None  # bash, sh, python, etc.
    continue_on_error: bool = False
    timeout_minutes: Optional[int] = None

    def __post_init__(self):
        if not self.name:
            # Generate name from command (first line, truncated)
            first_line = self.run.split('\n')[0].strip()
            self.name = first_line[:50] + ('...' if len(first_line) > 50 else '')


@dataclass
class Artifact:
    """
    Artifact configuration for a job.

    Artifacts are files/directories that are:
    - Produced by a job (paths)
    - Consumed from another job (consume)
    """
    paths: list[str] = field(default_factory=list)
    consume: list[str] = field(default_factory=list)  # Job names to consume artifacts from
    name: Optional[str] = None  # Custom artifact name
    retention_days: int = 7

    @property
    def has_outputs(self) -> bool:
        return len(self.paths) > 0

    @property
    def has_inputs(self) -> bool:
        return len(self.consume) > 0


@dataclass
class CoverageConfig:
    """Coverage report configuration."""
    path: str
    format: str = "auto"  # cobertura, lcov, or auto
    threshold: Optional[float] = None  # Fail if coverage below this percentage


@dataclass
class ReportConfig:
    """
    Test report configuration for a job.

    Supports JUnit XML for test results and Cobertura/lcov for coverage.

    Example:
        reports:
          junit: test-results/*.xml
          coverage:
            path: coverage.xml
            format: cobertura
            threshold: 80
    """
    junit: Optional[str] = None  # Path/glob to JUnit XML file(s)
    coverage: Optional[CoverageConfig] = None

    @property
    def has_junit(self) -> bool:
        return self.junit is not None

    @property
    def has_coverage(self) -> bool:
        return self.coverage is not None


@dataclass
class Service:
    """
    A service container that runs alongside jobs.

    Services are typically databases, caches, or other
    dependencies needed during job execution.
    """
    image: str
    name: Optional[str] = None
    env: dict[str, str] = field(default_factory=dict)
    ports: list[str] = field(default_factory=list)  # ["5432:5432"]
    volumes: list[str] = field(default_factory=list)  # ["/data:/var/lib/data"]
    options: list[str] = field(default_factory=list)  # Additional docker options
    healthcheck: Optional[dict[str, Any]] = None

    def __post_init__(self):
        if not self.name:
            # Generate name from image (without tag)
            self.name = self.image.split(':')[0].split('/')[-1]


@dataclass
class MatrixConfig:
    """
    Matrix configuration for running a job multiple times
    with different variable combinations.

    Example:
        matrix:
          python: [3.9, 3.10, 3.11]
          os: [ubuntu, alpine]

        Results in 6 job runs (3 python × 2 os)
    """
    variables: dict[str, list[Any]] = field(default_factory=dict)
    include: list[dict[str, Any]] = field(default_factory=list)  # Additional combinations
    exclude: list[dict[str, Any]] = field(default_factory=list)  # Combinations to skip

    def expand(self) -> list[dict[str, Any]]:
        """Expand matrix into list of variable combinations."""
        if not self.variables:
            return [{}]

        # Generate all combinations
        keys = list(self.variables.keys())
        combinations = []

        def generate(index: int, current: dict):
            if index == len(keys):
                combinations.append(current.copy())
                return

            key = keys[index]
            for value in self.variables[key]:
                current[key] = value
                generate(index + 1, current)
                del current[key]

        generate(0, {})

        # Add explicit includes
        for inc in self.include:
            if inc not in combinations:
                combinations.append(inc)

        # Remove explicit excludes
        for exc in self.exclude:
            combinations = [c for c in combinations if not all(
                c.get(k) == v for k, v in exc.items()
            )]

        return combinations


@dataclass
class Job:
    """
    A job within a pipeline stage.

    Jobs run in containers and consist of multiple steps.
    Jobs can depend on other jobs via 'needs'.
    """
    name: str
    stage: str
    image: str
    steps: list[Step] = field(default_factory=list)

    # Dependencies
    needs: list[str] = field(default_factory=list)

    # Artifacts
    artifacts: Optional[Artifact] = None

    # Test reports
    reports: Optional[ReportConfig] = None

    # Matrix builds
    matrix: Optional[MatrixConfig] = None

    # Conditional execution
    when: Optional[str] = None  # Simple condition: branch == 'main'
    condition: Optional[str] = None  # Complex if expression: ${{ success() && ... }}

    # Environment
    env: dict[str, str] = field(default_factory=dict)
    working_directory: str = "/workspace"

    # Execution settings
    timeout_minutes: int = 60
    continue_on_error: bool = False
    retry: int = 0  # Number of retries on failure
    parallel: bool = False  # Run in parallel with other parallel jobs

    # Runtime status (not from YAML)
    status: JobStatus = field(default=JobStatus.PENDING)

    def get_expanded_jobs(self) -> list['Job']:
        """
        If this job has a matrix, return expanded jobs.
        Otherwise, return a list containing just this job.
        """
        if not self.matrix:
            return [self]

        expanded = []
        for combo in self.matrix.expand():
            # Create a copy of the job with matrix variables applied
            suffix = '-'.join(f"{k}-{v}" for k, v in combo.items())

            # Substitute matrix variables in image name
            expanded_image = self.image
            for var, value in combo.items():
                expanded_image = expanded_image.replace(f"${{{var}}}", str(value))
                expanded_image = expanded_image.replace(f"${var}", str(value))

            new_job = Job(
                name=f"{self.name}-{suffix}",
                stage=self.stage,
                image=expanded_image,
                steps=self.steps.copy(),
                needs=self.needs.copy(),
                artifacts=self.artifacts,
                reports=self.reports,
                matrix=None,  # Expanded job doesn't have matrix
                when=self.when,
                condition=self.condition,
                env={**self.env, **{k.upper(): str(v) for k, v in combo.items()}},
                working_directory=self.working_directory,
                timeout_minutes=self.timeout_minutes,
                continue_on_error=self.continue_on_error,
                retry=self.retry,
                parallel=self.parallel,
            )
            expanded.append(new_job)

        return expanded


@dataclass
class Stage:
    """
    A stage in the pipeline.

    Stages run sequentially. Jobs within a stage can run
    in parallel (respecting dependencies).
    """
    name: str
    jobs: list[Job] = field(default_factory=list)
    services: list[Service] = field(default_factory=list)

    # Conditional execution
    when: Optional[str] = None
    condition: Optional[str] = None


@dataclass
class Pipeline:
    """
    The root pipeline configuration.

    A pipeline consists of:
    - Stages that run sequentially
    - Jobs that run within stages
    - Services that support job execution
    """
    name: str
    version: str = "1"
    stages: list[Stage] = field(default_factory=list)

    # Global environment variables
    env: dict[str, str] = field(default_factory=dict)

    # Default values for jobs
    defaults: dict[str, Any] = field(default_factory=dict)

    # Triggers (for export purposes)
    on: dict[str, Any] = field(default_factory=dict)

    def get_stage(self, name: str) -> Optional[Stage]:
        """Get a stage by name."""
        for stage in self.stages:
            if stage.name == name:
                return stage
        return None

    def get_job(self, name: str) -> Optional[Job]:
        """Get a job by name across all stages."""
        for stage in self.stages:
            for job in stage.jobs:
                if job.name == name:
                    return job
        return None

    def get_all_jobs(self) -> list[Job]:
        """Get all jobs across all stages."""
        jobs = []
        for stage in self.stages:
            jobs.extend(stage.jobs)
        return jobs

    def get_stage_names(self) -> list[str]:
        """Get ordered list of stage names."""
        return [stage.name for stage in self.stages]
